├── log
└── .keep
├── tmp
└── .keep
├── vendor
└── .keep
├── lib
└── tasks
│ └── .keep
├── .ruby-version
├── app
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── todo.rb
│ └── user.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── users_controller.rb
│ ├── admin
│ │ ├── users
│ │ │ └── todos_controller.rb
│ │ └── users_controller.rb
│ ├── refresh_controller.rb
│ ├── todos_controller.rb
│ ├── password_resets_controller.rb
│ ├── signup_controller.rb
│ ├── application_controller.rb
│ └── signin_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ └── mailer.html.erb
│ └── user_mailer
│ │ └── reset_password.text.erb
├── jobs
│ └── application_job.rb
├── errors
│ └── reset_password_error.rb
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
└── mailers
│ ├── application_mailer.rb
│ └── user_mailer.rb
├── todos-vue
├── static
│ └── .gitkeep
├── .eslintignore
├── config
│ ├── prod.env.js
│ ├── test.env.js
│ ├── dev.env.js
│ └── index.js
├── build
│ ├── logo.png
│ ├── vue-loader.conf.js
│ ├── webpack.test.conf.js
│ ├── build.js
│ ├── check-versions.js
│ ├── webpack.base.conf.js
│ ├── utils.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── src
│ ├── assets
│ │ └── logo.png
│ ├── App.vue
│ ├── main.js
│ ├── store.js
│ ├── components
│ │ ├── ForgotPassword.vue
│ │ ├── admin
│ │ │ └── users
│ │ │ │ ├── todos
│ │ │ │ └── List.vue
│ │ │ │ ├── List.vue
│ │ │ │ └── Edit.vue
│ │ ├── AppHeader.vue
│ │ ├── ResetPassword.vue
│ │ ├── Signin.vue
│ │ ├── Signup.vue
│ │ └── todos
│ │ │ └── List.vue
│ ├── router
│ │ └── index.js
│ └── backend
│ │ └── axios
│ │ └── index.js
├── test
│ └── unit
│ │ ├── .eslintrc
│ │ ├── specs
│ │ └── Signin.spec.js
│ │ ├── index.js
│ │ └── karma.conf.js
├── .editorconfig
├── .gitignore
├── .postcssrc.js
├── .babelrc
├── README.md
├── index.html
├── .eslintrc.js
└── package.json
├── .rspec
├── config
├── initializers
│ ├── jwt_sessions.rb
│ ├── mime_types.rb
│ ├── filter_parameter_logging.rb
│ ├── application_controller_renderer.rb
│ ├── backtrace_silencers.rb
│ ├── wrap_parameters.rb
│ ├── cors.rb
│ └── inflections.rb
├── spring.rb
├── environment.rb
├── boot.rb
├── cable.yml
├── credentials.yml.enc
├── database.yml
├── routes.rb
├── locales
│ └── en.yml
├── storage.yml
├── application.rb
├── puma.rb
└── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
├── public
└── robots.txt
├── spec
├── factories
│ ├── todos.rb
│ └── users.rb
├── support
│ ├── response_helper.rb
│ └── auth_helper.rb
├── models
│ ├── todo_spec.rb
│ └── user_spec.rb
├── requests
│ └── todos_spec.rb
├── controllers
│ ├── users_controller_spec.rb
│ ├── signup_controller_spec.rb
│ ├── admin
│ │ ├── users
│ │ │ └── todos_controller_spec.rb
│ │ └── users_controller_spec.rb
│ ├── signin_controller_spec.rb
│ ├── refresh_controller_spec.rb
│ ├── password_resets_controller_spec.rb
│ └── todos_controller_spec.rb
├── routing
│ └── todos_routing_spec.rb
├── rails_helper.rb
└── spec_helper.rb
├── bin
├── bundle
├── rake
├── rails
├── spring
├── update
└── setup
├── config.ru
├── db
├── migrate
│ ├── 20180622175133_add_email_index_to_users.rb
│ ├── 20180622170150_add_role_to_users.rb
│ ├── 20180615171006_create_todos.rb
│ ├── 20180615165435_create_users.rb
│ └── 20180704144112_add_reset_password_fields.rb
├── seeds.rb
└── schema.rb
├── Rakefile
├── README.md
├── .gitignore
├── Gemfile
└── Gemfile.lock
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.4.4
2 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/todos-vue/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/config/initializers/jwt_sessions.rb:
--------------------------------------------------------------------------------
1 | JWTSessions.encryption_key = 'secret'
2 |
--------------------------------------------------------------------------------
/app/errors/reset_password_error.rb:
--------------------------------------------------------------------------------
1 | class ResetPasswordError < StandardError
2 | end
3 |
--------------------------------------------------------------------------------
/todos-vue/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /config/
3 | /dist/
4 | /*.js
5 | /test/unit/coverage/
6 |
--------------------------------------------------------------------------------
/todos-vue/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/todos-vue/build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuwukee/silver-octo-invention/HEAD/todos-vue/build/logo.png
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/todos-vue/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tuwukee/silver-octo-invention/HEAD/todos-vue/src/assets/logo.png
--------------------------------------------------------------------------------
/app/models/todo.rb:
--------------------------------------------------------------------------------
1 | class Todo < ApplicationRecord
2 | belongs_to :user
3 |
4 | validates :title, presence: true
5 | end
6 |
--------------------------------------------------------------------------------
/spec/factories/todos.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :todo do
3 | title 'MyString'
4 | user
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/response_helper.rb:
--------------------------------------------------------------------------------
1 | module ResponseHelper
2 | def response_json
3 | JSON.parse(response.body) rescue {}
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w[
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ].each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/spec/models/todo_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe Todo, type: :model do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe User, type: :model do
4 | pending "add some examples to (or delete) #{__FILE__}"
5 | end
6 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/factories/users.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :user do
3 | sequence(:email) { |n| "email-#{n}@test.com" }
4 | password 'password'
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/todos-vue/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/db/migrate/20180622175133_add_email_index_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddEmailIndexToUsers < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :users, :email
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | before_action :authorize_access_request!
3 |
4 | def me
5 | render json: current_user
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | class UserMailer < ApplicationMailer
2 | def reset_password(user)
3 | @user = user
4 | mail(to: @user.email, subject: 'Reset your password')
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/db/migrate/20180622170150_add_role_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddRoleToUsers < ActiveRecord::Migration[5.2]
2 | def change
3 | add_column :users, :role, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/todos-vue/config/test.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const devEnv = require('./dev.env')
4 |
5 | module.exports = merge(devEnv, {
6 | NODE_ENV: '"testing"'
7 | })
8 |
--------------------------------------------------------------------------------
/todos-vue/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/todos-vue/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: silver-octo-invention_production
11 |
--------------------------------------------------------------------------------
/todos-vue/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | /test/unit/coverage/
8 |
9 | # Editor directories and files
10 | .idea
11 | .vscode
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 |
--------------------------------------------------------------------------------
/db/migrate/20180615171006_create_todos.rb:
--------------------------------------------------------------------------------
1 | class CreateTodos < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :todos do |t|
4 | t.string :title
5 | t.references :user, foreign_key: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/db/migrate/20180615165435_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :users do |t|
4 | t.string :email, default: '', null: false
5 | t.string :password_digest
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/requests/todos_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe "Todos", type: :request do
4 | describe "GET /todos" do
5 | xit "works! (now write some real specs)" do
6 | get todos_path
7 | expect(response).to have_http_status(200)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/user_mailer/reset_password.text.erb:
--------------------------------------------------------------------------------
1 | Hi <%= @user.email %>,
2 |
3 | You have requested to reset your password.
4 | Please follow this link:
5 | <%= "http://localhost:8080/#/password_resets/#{@user.reset_password_token}" %>
6 | Reset password URL is valid within 24 hours.
7 |
8 | Have a nice day!
9 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../config/application', __dir__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/todos-vue/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/db/migrate/20180704144112_add_reset_password_fields.rb:
--------------------------------------------------------------------------------
1 | class AddResetPasswordFields < ActiveRecord::Migration[5.2]
2 | def change
3 | add_column :users, :reset_password_token, :string, default: nil
4 | add_column :users, :reset_password_token_expires_at, :datetime, default: nil
5 | add_index :users, :reset_password_token
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/auth_helper.rb:
--------------------------------------------------------------------------------
1 | module AuthHelper
2 | def sign_in_as(user)
3 | payload = { user_id: user.id, aud: [user.role] }
4 | session = JWTSessions::Session.new(payload: payload)
5 | tokens = session.login
6 | request.cookies[JWTSessions.access_cookie] = tokens[:access]
7 | request.headers[JWTSessions.csrf_header] = tokens[:csrf]
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
7 | # Character.create(name: 'Luke', movie: movies.first)
8 |
--------------------------------------------------------------------------------
/todos-vue/test/unit/specs/Signin.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Signin from '@/components/Signin'
3 |
4 | describe('Signin.vue', () => {
5 | it('should render correct contents', () => {
6 | const Constructor = Vue.extend(Signin)
7 | const vm = new Constructor().$mount()
8 | expect(vm.$el.querySelector('form a')[0].textContent)
9 | .to.equal('New around here? Sign up')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/todos-vue/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
6 | }
7 | }],
8 | "stage-2"
9 | ],
10 | "plugins": ["transform-vue-jsx", "transform-runtime"],
11 | "env": {
12 | "test": {
13 | "presets": ["env", "stage-2"],
14 | "plugins": ["transform-vue-jsx", "istanbul"]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | OjywsELKrQzf+geWVtoxJOAEvlJxNc0uNc4XZ2/sS38UwqWHkmJFov6vGEP8Qe9ITKIebpxlWZol1y2EbgDnYt/NwLwFlNJp6lZGnYDq9szsIEdLWyz5fhfltew9JiPPLs7GTPnx83EovTPEaA7yNNmdot2wOvCiQy5cfyF8T5Ocq1z2Aud5CxuMka+M+9Xg+Zctrw7kDV770FAQl/Mlu45ZKpiHl1N2yWCl/CHbchGboMLihxGfkfLsNrEFIzhQAdl/m97WXx/mt+9mmfrWQLx8dhOqWok9POk+DOJ18rUp+N21vMgjK+Q1nu/uVclu8BrRyZtaQbixJvZkX1wXaNnc1UhXfmZDPJC8GgsZLaoQkwVm34RphDBZBHV0CjoBmt9s7LQznRkSyQx3XoewLBvoZ3VOxmumJMZr--kwKcuIs+oyhNOHfl--Bkn+ghdTqsoYcVdugmgdOg==
--------------------------------------------------------------------------------
/spec/controllers/users_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe UsersController, type: :controller do
4 | let(:user) { create(:user) }
5 | before { sign_in_as(user) }
6 |
7 | describe 'GET #me' do
8 | let!(:todo) { create(:todo, user: user) }
9 |
10 | it 'returns a success response' do
11 | get :me
12 | expect(response).to be_successful
13 | expect(response_json).to eq user.as_json.stringify_keys
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/controllers/admin/users/todos_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::Users::TodosController < ApplicationController
2 | before_action :authorize_access_request!
3 | before_action :set_user
4 | ROLES = %w[admin].freeze
5 |
6 | def index
7 | render json: @user.todos
8 | end
9 |
10 | def token_claims
11 | {
12 | aud: ROLES,
13 | verify_aud: true
14 | }
15 | end
16 |
17 | private
18 |
19 | def set_user
20 | @user = User.find(params[:user_id])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/todos-vue/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | Vue.config.productionTip = false
4 |
5 | // require all test files (files that ends with .spec.js)
6 | const testsContext = require.context('./specs', true, /\.spec$/)
7 | testsContext.keys().forEach(testsContext)
8 |
9 | // require all src files except main.js for coverage.
10 | // you can also change this to match only the subset of files that
11 | // you want coverage for.
12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
13 | srcContext.keys().forEach(srcContext)
14 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | if spring
13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
14 | gem 'spring', spring.version
15 | require 'spring/binstub'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | # ActiveSupport.on_load(:action_controller) do
8 | # wrap_parameters format: [:json]
9 | # end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/todos-vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 |
15 |
31 |
--------------------------------------------------------------------------------
/config/initializers/cors.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Avoid CORS issues when API is called from the frontend app.
4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
5 |
6 | # Read more: https://github.com/cyu/rack-cors
7 |
8 | Rails.application.config.middleware.insert_before 0, Rack::Cors do
9 | allow do
10 | origins 'http://localhost:8080'
11 |
12 | resource '*',
13 | headers: :any,
14 | credentials: true,
15 | methods: [:get, :post, :put, :patch, :delete, :options, :head]
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/todos-vue/README.md:
--------------------------------------------------------------------------------
1 | # todos-vue
2 |
3 | > Todos Vue.js application
4 |
5 | ## Build Setup
6 |
7 | ``` bash
8 | # install dependencies
9 | npm install
10 |
11 | # serve with hot reload at localhost:8080
12 | npm run dev
13 |
14 | # build for production with minification
15 | npm run build
16 |
17 | # build for production and view the bundle analyzer report
18 | npm run build --report
19 |
20 | # run unit tests
21 | npm run unit
22 |
23 | # run all tests
24 | npm test
25 | ```
26 |
27 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
28 |
--------------------------------------------------------------------------------
/todos-vue/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 | const sourceMapEnabled = isProduction
6 | ? config.build.productionSourceMap
7 | : config.dev.cssSourceMap
8 |
9 | module.exports = {
10 | loaders: utils.cssLoaders({
11 | sourceMap: sourceMapEnabled,
12 | extract: isProduction
13 | }),
14 | cssSourceMap: sourceMapEnabled,
15 | cacheBusting: config.dev.cacheBusting,
16 | transformToRequire: {
17 | video: ['src', 'poster'],
18 | source: 'src',
19 | img: 'src',
20 | image: 'xlink:href'
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rails + JWT + VueJS todos app
2 |
3 | [Rails API + JWT auth + VueJS SPA](https://blog.usejournal.com/rails-api-jwt-auth-vuejs-spa-eb4cf740a3ae)\
4 | [Rails API + JWT auth + VueJS SPA: Part 2, Roles](https://medium.com/@yuliaoletskaya/rails-api-jwt-auth-vuejs-spa-part-2-roles-601e4372a7e7)\
5 | [Rails API + JWT auth + VueJS SPA: Part 3, Passwords management](https://medium.com/@yuliaoletskaya/rails-api-jwt-auth-vuejs-spa-part-3-passwords-and-tokens-management-c1eddc6a49d1)
6 |
7 | Run rails
8 |
9 | ```
10 | $ bundle install
11 | $ rails db:create
12 | $ rails db:migrate
13 | $ rails s
14 | ```
15 |
16 | Run VueJS app
17 |
18 | ```
19 | $ cd todos-vue
20 | $ npm install
21 | $ npm run dev
22 | ```
23 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/spec/controllers/signup_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe SignupController, type: :controller do
4 |
5 | describe 'POST #create' do
6 | let(:user_params) { { email: 'test@email.com', password: 'password', password_confirmation: 'password' } }
7 |
8 | it 'returns http success' do
9 | post :create, params: user_params
10 | expect(response).to be_successful
11 | expect(response_json.keys).to eq ['csrf']
12 | expect(response.cookies[JWTSessions.access_cookie]).to be_present
13 | end
14 |
15 | it 'creates a new user' do
16 | expect do
17 | post :create, params: user_params
18 | end.to change(User, :count).by(1)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | post 'refresh', controller: :refresh, action: :create
3 | post 'signin', controller: :signin, action: :create
4 | post 'signup', controller: :signup, action: :create
5 | delete 'signin', controller: :signin, action: :destroy
6 | get 'me', controller: :users, action: :me
7 |
8 | resources :todos
9 | resources :password_resets, only: [:create] do
10 | collection do
11 | get ':token', action: :edit, as: :edit
12 | patch ':token', action: :update
13 | end
14 | end
15 |
16 | namespace :admin do
17 | resources :users, only: [:index, :show, :update] do
18 | resources :todos, only: [:index], controller: 'users/todos'
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | # Ignore uploaded files in development
21 | /storage/*
22 |
23 | .byebug_history
24 |
25 | # Ignore master key for decrypting credentials and more.
26 | /config/master.key
27 |
--------------------------------------------------------------------------------
/todos-vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | todos-vue
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/todos-vue/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | import App from './App'
5 | import router from './router'
6 | import { store } from './store'
7 | import VueAxios from 'vue-axios'
8 | import { securedAxiosInstance, plainAxiosInstance } from './backend/axios'
9 |
10 | Vue.config.productionTip = false
11 | Vue.use(VueAxios, {
12 | secured: securedAxiosInstance,
13 | plain: plainAxiosInstance
14 | })
15 |
16 | /* eslint-disable no-new */
17 | new Vue({
18 | el: '#app',
19 | router,
20 | store,
21 | securedAxiosInstance,
22 | plainAxiosInstance,
23 | components: { App },
24 | template: ''
25 | })
26 |
--------------------------------------------------------------------------------
/app/controllers/refresh_controller.rb:
--------------------------------------------------------------------------------
1 | class RefreshController < ApplicationController
2 | before_action :authorize_refresh_by_access_request!
3 |
4 | def create
5 | session = JWTSessions::Session.new(payload: claimless_payload,
6 | refresh_by_access_allowed: true,
7 | namespace: "user_#{claimless_payload['user_id']}")
8 | tokens = session.refresh_by_access_payload do
9 | raise JWTSessions::Errors::Unauthorized, 'Malicious activity detected'
10 | end
11 | response.set_cookie(JWTSessions.access_cookie,
12 | value: tokens[:access],
13 | httponly: true,
14 | secure: Rails.env.production?)
15 |
16 | render json: { csrf: tokens[:csrf] }
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a way to update your development environment automatically.
14 | # Add necessary update steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | puts "\n== Updating database =="
21 | system! 'bin/rails db:migrate'
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system! 'bin/rails log:clear tmp:clear'
25 |
26 | puts "\n== Restarting application server =="
27 | system! 'bin/rails restart'
28 | end
29 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | include ActiveModel::Serializers::JSON
3 | has_secure_password
4 | has_many :todos
5 |
6 | enum role: %i[user manager admin].freeze
7 |
8 | validates :email,
9 | format: { with: URI::MailTo::EMAIL_REGEXP },
10 | presence: true,
11 | uniqueness: { case_sensitive: false }
12 |
13 | def attributes
14 | { id: id, email: email, role: role }
15 | end
16 |
17 | def generate_password_token!
18 | begin
19 | self.reset_password_token = SecureRandom.urlsafe_base64
20 | end while User.exists?(reset_password_token: self.reset_password_token)
21 | self.reset_password_token_expires_at = 1.day.from_now
22 | save!
23 | end
24 |
25 | def clear_password_token!
26 | self.reset_password_token = nil
27 | self.reset_password_token_expires_at = nil
28 | save!
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/todos-vue/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint'
7 | },
8 | env: {
9 | browser: true,
10 | },
11 | extends: [
12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
14 | 'plugin:vue/essential',
15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md
16 | 'standard'
17 | ],
18 | // required to lint *.vue files
19 | plugins: [
20 | 'vue'
21 | ],
22 | // add your custom rules here
23 | rules: {
24 | // allow async-await
25 | 'generator-star-spacing': 'off',
26 | // allow debugger during development
27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/spec/routing/todos_routing_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe TodosController, type: :routing do
4 | describe "routing" do
5 |
6 | it "routes to #index" do
7 | expect(:get => "/todos").to route_to("todos#index")
8 | end
9 |
10 |
11 | it "routes to #show" do
12 | expect(:get => "/todos/1").to route_to("todos#show", :id => "1")
13 | end
14 |
15 |
16 | it "routes to #create" do
17 | expect(:post => "/todos").to route_to("todos#create")
18 | end
19 |
20 | it "routes to #update via PUT" do
21 | expect(:put => "/todos/1").to route_to("todos#update", :id => "1")
22 | end
23 |
24 | it "routes to #update via PATCH" do
25 | expect(:patch => "/todos/1").to route_to("todos#update", :id => "1")
26 | end
27 |
28 | it "routes to #destroy" do
29 | expect(:delete => "/todos/1").to route_to("todos#destroy", :id => "1")
30 | end
31 |
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/controllers/todos_controller.rb:
--------------------------------------------------------------------------------
1 | class TodosController < ApplicationController
2 | before_action :authorize_access_request!
3 | before_action :set_todo, only: [:show, :update, :destroy]
4 |
5 | # GET /todos
6 | def index
7 | @todos = current_user.todos
8 |
9 | render json: @todos
10 | end
11 |
12 | # GET /todos/1
13 | def show
14 | render json: @todo
15 | end
16 |
17 | # POST /todos
18 | def create
19 | @todo = current_user.todos.build(todo_params)
20 | @todo.save!
21 | render json: @todo, status: :created, location: @todo
22 | end
23 |
24 | # PATCH/PUT /todos/1
25 | def update
26 | @todo.update!(todo_params)
27 | render json: @todo
28 | end
29 |
30 | # DELETE /todos/1
31 | def destroy
32 | @todo.destroy
33 | end
34 |
35 | private
36 |
37 | def set_todo
38 | @todo = current_user.todos.find(params[:id])
39 | end
40 |
41 | def todo_params
42 | params.require(:todo).permit(:title)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at http://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a starting point to setup your application.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?('config/database.yml')
22 | # cp 'config/database.yml.sample', 'config/database.yml'
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! 'bin/rails db:setup'
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! 'bin/rails log:clear tmp:clear'
30 |
31 | puts "\n== Restarting application server =="
32 | system! 'bin/rails restart'
33 | end
34 |
--------------------------------------------------------------------------------
/todos-vue/build/webpack.test.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // This is the webpack config used for unit tests.
3 |
4 | const utils = require('./utils')
5 | const webpack = require('webpack')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 |
9 | const webpackConfig = merge(baseWebpackConfig, {
10 | // use inline sourcemap for karma-sourcemap-loader
11 | module: {
12 | rules: utils.styleLoaders()
13 | },
14 | devtool: '#inline-source-map',
15 | resolveLoader: {
16 | alias: {
17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option
18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724
19 | 'scss-loader': 'sass-loader'
20 | }
21 | },
22 | plugins: [
23 | new webpack.DefinePlugin({
24 | 'process.env': require('../config/test.env')
25 | })
26 | ]
27 | })
28 |
29 | // no need for app entry during tests
30 | delete webpackConfig.entry
31 |
32 | module.exports = webpackConfig
33 |
--------------------------------------------------------------------------------
/app/controllers/password_resets_controller.rb:
--------------------------------------------------------------------------------
1 | class PasswordResetsController < ApplicationController
2 | before_action :set_user, only: [:edit, :update]
3 | KEYS = [:password, :password_confirmation].freeze
4 |
5 | def create
6 | user = User.find_by(email: params[:email])
7 | if user
8 | user.generate_password_token!
9 | UserMailer.reset_password(user).deliver_now
10 | end
11 |
12 | render json: :ok
13 | end
14 |
15 | def edit
16 | render json: :ok
17 | end
18 |
19 | def update
20 | @user.update!(password_params)
21 | @user.clear_password_token!
22 | JWTSessions::Session.new(namespace: "user_#{@user.id}").flush_namespaced
23 | render json: :ok
24 | end
25 |
26 | private
27 |
28 | def password_params
29 | params.tap { |p| p.require(KEYS) }.permit(*KEYS)
30 | end
31 |
32 | def set_user
33 | @user = User.find_by(reset_password_token: params[:token])
34 | raise ResetPasswordError unless @user&.reset_password_token_expires_at && @user.reset_password_token_expires_at > Time.now
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/controllers/signup_controller.rb:
--------------------------------------------------------------------------------
1 | class SignupController < ApplicationController
2 | KEYS = [:email, :password, :password_confirmation].freeze
3 |
4 | def create
5 | user = User.new(user_params)
6 | if user.save
7 | payload = { user_id: user.id, aud: [user.role] }
8 | session = JWTSessions::Session.new(payload: payload,
9 | refresh_by_access_allowed: true,
10 | namespace: "user_#{user.id}")
11 | tokens = session.login
12 |
13 | response.set_cookie(JWTSessions.access_cookie,
14 | value: tokens[:access],
15 | httponly: true,
16 | secure: Rails.env.production?)
17 | render json: { csrf: tokens[:csrf] }
18 | else
19 | render json: { error: user.errors.full_messages.join(' ') },
20 | status: :unprocessable_entity
21 | end
22 | end
23 |
24 | private
25 |
26 | def user_params
27 | params.tap { |p| p.require(KEYS) }.permit(*KEYS)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/todos-vue/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import createPersistedState from 'vuex-persistedstate'
4 | Vue.use(Vuex)
5 |
6 | export const store = new Vuex.Store({
7 | state: {
8 | currentUser: {},
9 | csrf: null,
10 | todos: []
11 | },
12 | getters: {
13 | isAdmin (state) {
14 | return state.currentUser && state.currentUser.role === 'admin'
15 | },
16 | isManager (state) {
17 | return state.currentUser && state.currentUser.role === 'manager'
18 | },
19 | currentUserId (state) {
20 | return state.currentUser && state.currentUser.id
21 | }
22 | },
23 | mutations: {
24 | setCurrentUser (state, { currentUser, csrf }) {
25 | state.currentUser = currentUser
26 | state.signedIn = true
27 | state.csrf = csrf
28 | },
29 | unsetCurrentUser (state) {
30 | state.currentUser = {}
31 | state.signedIn = false
32 | state.csrf = null
33 | },
34 | refresh (state, csrf) {
35 | state.signedIn = true
36 | state.csrf = csrf
37 | }
38 | },
39 | plugins: [createPersistedState()]
40 | })
41 |
--------------------------------------------------------------------------------
/spec/controllers/admin/users/todos_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe Admin::Users::TodosController, type: :controller do
4 | let(:user) { create(:user) }
5 | let(:manager) { create(:user, role: :manager) }
6 | let(:admin) { create(:user, role: :admin) }
7 | let!(:todo) { create(:todo, user: user) }
8 | let!(:todo2) { create(:todo, user: manager) }
9 |
10 | describe 'GET #index' do
11 | it 'allows admin to receive todos list' do
12 | sign_in_as(admin)
13 | get :index, params: { user_id: user.id }
14 | expect(response).to be_successful
15 | expect(response_json.size).to eq 1
16 | expect(response_json.first['id']).to eq todo.id
17 | end
18 |
19 | it 'allows manager to receive users list' do
20 | sign_in_as(manager)
21 | get :index, params: { user_id: user.id }
22 | expect(response).to have_http_status(403)
23 | end
24 |
25 | it 'does not allow regular user to receive users list' do
26 | sign_in_as(user)
27 | get :index, params: { user_id: user.id }
28 | expect(response).to have_http_status(403)
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/todos-vue/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | var webpackConfig = require('../../build/webpack.test.conf')
7 |
8 | module.exports = function karmaConfig (config) {
9 | config.set({
10 | // to run in additional browsers:
11 | // 1. install corresponding karma launcher
12 | // http://karma-runner.github.io/0.13/config/browsers.html
13 | // 2. add it to the `browsers` array below.
14 | browsers: ['PhantomJS'],
15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
16 | reporters: ['spec', 'coverage'],
17 | files: ['./index.js'],
18 | preprocessors: {
19 | './index.js': ['webpack', 'sourcemap']
20 | },
21 | webpack: webpackConfig,
22 | webpackMiddleware: {
23 | noInfo: true
24 | },
25 | coverageReporter: {
26 | dir: './coverage',
27 | reporters: [
28 | { type: 'lcov', subdir: '.' },
29 | { type: 'text-summary' }
30 | ]
31 | }
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/app/controllers/admin/users_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::UsersController < ApplicationController
2 | before_action :authorize_access_request!
3 | before_action :set_user, only: [:show, :update]
4 | VIEW_ROLES = %w[admin manager].freeze
5 | EDIT_ROLES = %w[admin].freeze
6 |
7 | def index
8 | @users = User.all
9 |
10 | render json: @users
11 | end
12 |
13 | def show
14 | render json: @user
15 | end
16 |
17 | def update
18 | if current_user.id != @user.id
19 | @user.update!(user_params)
20 | JWTSessions::Session.new(namespace: "user_#{@user.id}").flush_namespaced_access_tokens
21 | render json: @user
22 | else
23 | render json: { error: 'Admin cannot modify their own role' }, status: :bad_request
24 | end
25 | end
26 |
27 | def token_claims
28 | {
29 | aud: allowed_aud,
30 | verify_aud: true
31 | }
32 | end
33 |
34 | private
35 |
36 | def allowed_aud
37 | action_name == 'update' ? EDIT_ROLES : VIEW_ROLES
38 | end
39 |
40 | def set_user
41 | @user = User.find(params[:id])
42 | end
43 |
44 | def user_params
45 | params.require(:user).permit(:role)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::API
2 | include JWTSessions::RailsAuthorization
3 | rescue_from ActionController::ParameterMissing, with: :bad_request
4 | rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
5 | rescue_from ActiveRecord::RecordNotFound, with: :not_found
6 | rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized
7 | rescue_from JWTSessions::Errors::ClaimsVerification, with: :forbidden
8 | rescue_from ResetPasswordError, with: :not_authorized
9 |
10 | private
11 |
12 | def current_user
13 | @current_user ||= User.find(payload['user_id'])
14 | end
15 |
16 | def bad_request
17 | render json: { error: 'Bad request' }, status: :bad_request
18 | end
19 |
20 | def forbidden
21 | render json: { error: 'Forbidden' }, status: :forbidden
22 | end
23 |
24 | def not_authorized
25 | render json: { error: 'Not authorized' }, status: :unauthorized
26 | end
27 |
28 | def not_found
29 | render json: { error: 'Not found' }, status: :not_found
30 | end
31 |
32 | def unprocessable_entity(exception)
33 | render json: { error: exception.record.errors.full_messages.join(' ') }, status: :unprocessable_entity
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/controllers/signin_controller.rb:
--------------------------------------------------------------------------------
1 | class SigninController < ApplicationController
2 | before_action :authorize_access_request!, only: [:destroy]
3 |
4 | def create
5 | user = User.find_by!(email: params[:email])
6 | if user.authenticate(params[:password])
7 | payload = { user_id: user.id, aud: [user.role] }
8 | session = JWTSessions::Session.new(payload: payload,
9 | refresh_by_access_allowed: true,
10 | namespace: "user_#{user.id}")
11 | tokens = session.login
12 |
13 | response.set_cookie(JWTSessions.access_cookie,
14 | value: tokens[:access],
15 | httponly: true,
16 | secure: Rails.env.production?)
17 | render json: { csrf: tokens[:csrf] }
18 | else
19 | not_authorized
20 | end
21 | end
22 |
23 | def destroy
24 | session = JWTSessions::Session.new(payload: payload, namespace: "user_#{payload['user_id']}")
25 | session.flush_by_access_payload
26 | render json: :ok
27 | end
28 |
29 | private
30 |
31 | def not_found
32 | render json: { error: 'Cannont find such email/password combination' }, status: :not_found
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby '2.4.4'
5 |
6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
7 | gem 'rails', '~> 5.2.0'
8 | # Use sqlite3 as the database for Active Record
9 | gem 'sqlite3'
10 | # Use Puma as the app server
11 | gem 'puma', '~> 3.12'
12 | # Use Redis adapter to run Action Cable in production
13 | gem 'redis', '~> 4.0'
14 | # Use ActiveModel has_secure_password
15 | gem 'bcrypt', '~> 3.1.7'
16 | # Use JWTSessions to build JWT auth
17 | gem 'jwt_sessions', '~> 2.4'
18 |
19 | # Reduces boot times through caching; required in config/boot.rb
20 | gem 'bootsnap', '>= 1.1.0', require: false
21 |
22 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
23 | gem 'rack-cors'
24 |
25 | group :development, :test do
26 | gem 'pry-byebug', '~> 3.4'
27 | gem 'pry-rails', '~> 0.3.4'
28 | gem 'rspec-rails', '~> 3.7'
29 | gem 'factory_bot_rails', '~> 4.8'
30 | end
31 |
32 | group :development do
33 | gem 'listen', '>= 3.0.5', '< 3.2'
34 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
35 | gem 'spring'
36 | gem 'spring-watcher-listen', '~> 2.0.0'
37 | end
38 |
--------------------------------------------------------------------------------
/todos-vue/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, (err, stats) => {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | require "active_storage/engine"
9 | require "action_controller/railtie"
10 | require "action_mailer/railtie"
11 | require "action_view/railtie"
12 | require "action_cable/engine"
13 | # require "sprockets/railtie"
14 | require "rails/test_unit/railtie"
15 |
16 | # Require the gems listed in Gemfile, including any gems
17 | # you've limited to :test, :development, or :production.
18 | Bundler.require(*Rails.groups)
19 |
20 | module SilverOctoInvention
21 | class Application < Rails::Application
22 | # Initialize configuration defaults for originally generated Rails version.
23 | config.load_defaults 5.2
24 |
25 | # Settings in config/environments/* take precedence over those specified here.
26 | # Application configuration can go into files in config/initializers
27 | # -- all .rb files in that directory are automatically loaded after loading
28 | # the framework and any gems in your application.
29 |
30 | # Only loads a smaller set of middleware suitable for API only apps.
31 | # Middleware like session, flash, cookies can be added back manually.
32 | # Skip views, helpers and assets when generating a new resource.
33 | config.api_only = true
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/todos-vue/src/components/ForgotPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
45 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory.
30 | #
31 | # preload_app!
32 |
33 | # Allow puma to be restarted by `rails restart` command.
34 | plugin :tmp_restart
35 |
--------------------------------------------------------------------------------
/todos-vue/src/components/admin/users/todos/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ error }}
5 |
User Todos
6 |
7 |
8 |
9 |
10 | | ID |
11 | User ID |
12 | Title |
13 |
14 |
15 |
16 |
17 | | {{ todo.id }} |
18 | {{ todo.user_id }} |
19 | {{ todo.title }} |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
54 |
--------------------------------------------------------------------------------
/todos-vue/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../package.json')
5 | const shell = require('shelljs')
6 |
7 | function exec (cmd) {
8 | return require('child_process').execSync(cmd).toString().trim()
9 | }
10 |
11 | const versionRequirements = [
12 | {
13 | name: 'node',
14 | currentVersion: semver.clean(process.version),
15 | versionRequirement: packageConfig.engines.node
16 | }
17 | ]
18 |
19 | if (shell.which('npm')) {
20 | versionRequirements.push({
21 | name: 'npm',
22 | currentVersion: exec('npm --version'),
23 | versionRequirement: packageConfig.engines.npm
24 | })
25 | }
26 |
27 | module.exports = function () {
28 | const warnings = []
29 |
30 | for (let i = 0; i < versionRequirements.length; i++) {
31 | const mod = versionRequirements[i]
32 |
33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34 | warnings.push(mod.name + ': ' +
35 | chalk.red(mod.currentVersion) + ' should be ' +
36 | chalk.green(mod.versionRequirement)
37 | )
38 | }
39 | }
40 |
41 | if (warnings.length) {
42 | console.log('')
43 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
44 | console.log()
45 |
46 | for (let i = 0; i < warnings.length; i++) {
47 | const warning = warnings[i]
48 | console.log(' ' + warning)
49 | }
50 |
51 | console.log()
52 | process.exit(1)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/todos-vue/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Signin from '@/components/Signin'
4 | import Signup from '@/components/Signup'
5 | import ForgotPassword from '@/components/ForgotPassword'
6 | import ResetPassword from '@/components/ResetPassword'
7 | import TodosList from '@/components/todos/List'
8 | import UsersList from '@/components/admin/users/List'
9 | import UserEdit from '@/components/admin/users/Edit'
10 | import UserTodosList from '@/components/admin/users/todos/List'
11 |
12 | Vue.use(Router)
13 |
14 | export default new Router({
15 | routes: [
16 | {
17 | path: '/',
18 | name: 'Signin',
19 | component: Signin
20 | },
21 | {
22 | path: '/signup',
23 | name: 'Signup',
24 | component: Signup
25 | },
26 | {
27 | path: '/todos',
28 | name: 'List',
29 | component: TodosList
30 | },
31 | {
32 | path: '/forgot_password',
33 | name: 'ForgotPassword',
34 | component: ForgotPassword
35 | },
36 | {
37 | path: '/password_resets/:token',
38 | name: 'ResetPassword',
39 | component: ResetPassword
40 | },
41 | {
42 | path: '/admin/users',
43 | name: 'UsersList',
44 | component: UsersList
45 | },
46 | {
47 | path: '/admin/users/:id/todos',
48 | name: 'UserTodosList',
49 | component: UserTodosList
50 | },
51 | {
52 | path: '/admin/users/:id',
53 | name: 'UserEdit',
54 | component: UserEdit
55 | }
56 | ]
57 | })
58 |
--------------------------------------------------------------------------------
/todos-vue/src/components/AppHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
40 |
41 |
60 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 2018_07_04_144112) do
14 |
15 | create_table "todos", force: :cascade do |t|
16 | t.string "title"
17 | t.integer "user_id"
18 | t.datetime "created_at", null: false
19 | t.datetime "updated_at", null: false
20 | t.index ["user_id"], name: "index_todos_on_user_id"
21 | end
22 |
23 | create_table "users", force: :cascade do |t|
24 | t.string "email", default: "", null: false
25 | t.string "password_digest"
26 | t.datetime "created_at", null: false
27 | t.datetime "updated_at", null: false
28 | t.integer "role", default: 0, null: false
29 | t.string "reset_password_token"
30 | t.datetime "reset_password_token_expires_at"
31 | t.index ["email"], name: "index_users_on_email"
32 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token"
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/spec/controllers/signin_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe SigninController, type: :controller do
4 | let(:user) { create(:user) }
5 |
6 | describe 'POST #create' do
7 | let(:password) { 'password' }
8 | let(:user_params) { { email: user.email, password: password } }
9 |
10 | it 'returns http success' do
11 | post :create, params: user_params
12 | expect(response).to be_successful
13 | expect(response_json.keys).to eq ['csrf']
14 | expect(response.cookies[JWTSessions.access_cookie]).to be_present
15 | end
16 |
17 | it 'returns unauthorized for invalid params' do
18 | post :create, params: { email: user.email, password: 'incorrect' }
19 | expect(response).to have_http_status(401)
20 | end
21 | end
22 |
23 | describe 'logout DELETE #destroy' do
24 | context 'failure' do
25 | it 'returns unauthorized http status' do
26 | delete :destroy
27 | expect(response).to have_http_status(401)
28 | end
29 | end
30 | context 'success' do
31 | it 'returns http success with valid tokens' do
32 | payload = { user_id: user.id }
33 |
34 | session = JWTSessions::Session.new(
35 | payload: payload,
36 | refresh_by_access_allowed: true,
37 | namespace: "user_#{user.id}"
38 | )
39 |
40 | tokens = session.login
41 | request.cookies[JWTSessions.access_cookie] = tokens[:access]
42 | request.headers[JWTSessions.csrf_header] = tokens[:csrf]
43 |
44 | delete :destroy
45 | expect(response).to have_http_status(200)
46 | expect(response_json).to eq('ok')
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/controllers/refresh_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe RefreshController, type: :controller do
4 | let(:access_cookie) { @tokens[:access] }
5 | let(:csrf_token) { @tokens[:csrf] }
6 |
7 | describe "POST #create" do
8 | let(:user) { create(:user) }
9 |
10 | context 'success' do
11 | before do
12 | # set expiration time to 0 to create an already expired access token
13 | JWTSessions.access_exp_time = 0
14 | payload = { user_id: user.id }
15 | session = JWTSessions::Session.new(payload: payload,
16 | refresh_by_access_allowed: true,
17 | namespace: "user_#{user.id}")
18 | @tokens = session.login
19 | JWTSessions.access_exp_time = 3600
20 | end
21 |
22 | it do
23 | request.cookies[JWTSessions.access_cookie] = access_cookie
24 | request.headers[JWTSessions.csrf_header] = csrf_token
25 | post :create
26 | expect(response).to be_successful
27 | expect(response_json.keys.sort).to eq ['csrf']
28 | expect(response.cookies[JWTSessions.access_cookie]).to be_present
29 | end
30 | end
31 |
32 | context 'failure' do
33 | before do
34 | payload = { user_id: user.id }
35 | session = JWTSessions::Session.new(payload: payload,
36 | refresh_by_access_allowed: true,
37 | namespace: "user_#{user.id}")
38 | @tokens = session.login
39 | end
40 |
41 | it do
42 | request.cookies[JWTSessions.access_cookie] = access_cookie
43 | request.headers[JWTSessions.csrf_header] = csrf_token
44 | post :create
45 | expect(response).to have_http_status(401)
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/todos-vue/src/backend/axios/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { store } from './../../store'
3 |
4 | const API_URL = 'http://localhost:3000'
5 |
6 | const securedAxiosInstance = axios.create({
7 | baseURL: API_URL,
8 | withCredentials: true,
9 | headers: {
10 | 'Content-Type': 'application/json'
11 | }
12 | })
13 |
14 | const plainAxiosInstance = axios.create({
15 | baseURL: API_URL,
16 | withCredentials: true,
17 | headers: {
18 | 'Content-Type': 'application/json'
19 | }
20 | })
21 |
22 | securedAxiosInstance.interceptors.request.use(config => {
23 | const method = config.method.toUpperCase()
24 | if (method !== 'OPTIONS' && method !== 'GET') {
25 | config.headers = {
26 | ...config.headers,
27 | 'X-CSRF-TOKEN': store.state.csrf
28 | }
29 | }
30 | return config
31 | })
32 |
33 | securedAxiosInstance.interceptors.response.use(null, error => {
34 | if (error.response && error.response.config && error.response.status === 401) {
35 | // In case 401 is caused by expired access cookie - we'll do refresh request
36 | return plainAxiosInstance.post('/refresh', {}, { headers: { 'X-CSRF-TOKEN': store.state.csrf } })
37 | .then(response => {
38 | plainAxiosInstance.get('/me')
39 | .then(meResponse => store.commit('setCurrentUser', { currentUser: meResponse.data, csrf: response.data.csrf }))
40 | // And after successful refresh - repeat the original request
41 | let retryConfig = error.response.config
42 | retryConfig.headers['X-CSRF-TOKEN'] = response.data.csrf
43 | return plainAxiosInstance.request(retryConfig)
44 | }).catch(error => {
45 | store.commit('unsetCurrentUser')
46 | // redirect to signin in case refresh request fails
47 | location.replace('/')
48 | return Promise.reject(error)
49 | })
50 | } else {
51 | return Promise.reject(error)
52 | }
53 | })
54 |
55 | export { securedAxiosInstance, plainAxiosInstance }
56 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 |
31 | # Store uploaded files on the local file system in a temporary directory
32 | config.active_storage.service = :test
33 |
34 | config.action_mailer.perform_caching = false
35 |
36 | # Tell Action Mailer not to deliver emails to the real world.
37 | # The :test delivery method accumulates sent emails in the
38 | # ActionMailer::Base.deliveries array.
39 | config.action_mailer.delivery_method = :test
40 |
41 | # Print deprecation notices to the stderr.
42 | config.active_support.deprecation = :stderr
43 |
44 | # Raises error for missing translations
45 | # config.action_view.raise_on_missing_translations = true
46 | end
47 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | # Run rails dev:cache to toggle caching.
17 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
18 | config.action_controller.perform_caching = true
19 |
20 | config.cache_store = :memory_store
21 | config.public_file_server.headers = {
22 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
23 | }
24 | else
25 | config.action_controller.perform_caching = false
26 |
27 | config.cache_store = :null_store
28 | end
29 |
30 | # Store uploaded files on the local file system (see config/storage.yml for options)
31 | config.active_storage.service = :local
32 |
33 | # Don't care if the mailer can't send.
34 | config.action_mailer.raise_delivery_errors = false
35 |
36 | config.action_mailer.perform_caching = false
37 |
38 | # Print deprecation notices to the Rails logger.
39 | config.active_support.deprecation = :log
40 |
41 | # Raise an error on page load if there are pending migrations.
42 | config.active_record.migration_error = :page_load
43 |
44 | # Highlight code that triggered database queries in logs.
45 | config.active_record.verbose_query_logs = true
46 |
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 | end
55 |
--------------------------------------------------------------------------------
/todos-vue/src/components/ResetPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
60 |
--------------------------------------------------------------------------------
/todos-vue/src/components/Signin.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
68 |
--------------------------------------------------------------------------------
/todos-vue/src/components/admin/users/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ error }}
5 |
Users
6 |
7 |
8 |
9 |
10 | | ID |
11 | Email |
12 | Role |
13 | Todos |
14 |
15 |
16 |
17 |
18 | | {{ user.id }} |
19 |
20 |
21 | {{ user.email }}
22 |
23 | |
24 |
25 | {{ user.email }}
26 | |
27 | {{ user.role }} |
28 |
29 |
30 |
31 |
32 | |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
73 |
74 |
80 |
--------------------------------------------------------------------------------
/spec/controllers/admin/users_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe Admin::UsersController, type: :controller do
4 | let!(:user) { create(:user) }
5 | let!(:manager) { create(:user, role: :manager) }
6 | let!(:admin) { create(:user, role: :admin) }
7 |
8 | describe 'GET #index' do
9 | it 'allows admin to receive users list' do
10 | sign_in_as(admin)
11 | get :index
12 | expect(response).to be_successful
13 | expect(response_json.size).to eq 3
14 | end
15 |
16 | it 'allows manager to receive users list' do
17 | sign_in_as(manager)
18 | get :index
19 | expect(response).to be_successful
20 | expect(response_json.size).to eq 3
21 | end
22 |
23 | it 'does not allow regular user to receive users list' do
24 | sign_in_as(user)
25 | get :index
26 | expect(response).to have_http_status(403)
27 | end
28 | end
29 |
30 | describe 'GET #show' do
31 | it 'allows admin to get a user' do
32 | sign_in_as(admin)
33 | get :show, params: { id: user.id }
34 | expect(response).to be_successful
35 | end
36 |
37 | it 'allows manager to get a user' do
38 | sign_in_as(manager)
39 | get :show, params: { id: user.id }
40 | expect(response).to be_successful
41 | end
42 |
43 | it 'does not allow regular user to get a user' do
44 | sign_in_as(user)
45 | get :show, params: { id: user.id }
46 | expect(response).to have_http_status(403)
47 | end
48 | end
49 |
50 | describe 'PATCH #update' do
51 | it 'allows admin to update a user' do
52 | sign_in_as(admin)
53 | patch :update, params: { id: user.id, user: { role: :manager } }
54 | expect(response).to be_successful
55 | expect(user.reload.role).to eq 'manager'
56 | end
57 |
58 | it 'does not allow manager to update a user' do
59 | sign_in_as(manager)
60 | patch :update, params: { id: user.id, user: { role: :manager } }
61 | expect(response).to have_http_status(403)
62 | end
63 |
64 | it 'does not allow user to update a user' do
65 | sign_in_as(user)
66 | patch :update, params: { id: user.id, user: { role: :manager } }
67 | expect(response).to have_http_status(403)
68 | end
69 |
70 | it 'does not allow admin to update their own role' do
71 | sign_in_as(admin)
72 | patch :update, params: { id: admin.id, user: { role: :manager } }
73 | expect(response).to have_http_status(400)
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/spec/controllers/password_resets_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe PasswordResetsController, type: :controller do
4 | let(:user) { create(:user) }
5 |
6 | describe "POST #create" do
7 | it do
8 | expect(UserMailer).to receive(:reset_password).once.and_return(double(deliver_now: true))
9 | post :create, params: { email: user.email }
10 | expect(response).to be_successful
11 | end
12 |
13 | it do
14 | expect(UserMailer).to_not receive(:reset_password)
15 | post :create, params: { email: 'non@existent.com' }
16 | expect(response).to be_successful
17 | end
18 | end
19 |
20 | describe "GET #edit" do
21 | it do
22 | user.generate_password_token!
23 | get :edit, params: { token: user.reset_password_token }
24 | expect(response).to be_successful
25 | end
26 |
27 | it 'returns unauthorized for expired tokens' do
28 | user.generate_password_token!
29 | user.update({ reset_password_token_expires_at: 2.days.ago })
30 | get :edit, params: { token: user.reset_password_token }
31 | expect(response).to have_http_status(401)
32 | end
33 |
34 | it 'returns unauthorized for invalid expirations' do
35 | user.generate_password_token!
36 | user.update({ reset_password_token_expires_at: nil })
37 | get :edit, params: { token: user.reset_password_token }
38 | expect(response).to have_http_status(401)
39 | end
40 |
41 | it 'returns unauthorized for invalid params' do
42 | user.generate_password_token!
43 | get :edit, params: { token: 1 }
44 | expect(response).to have_http_status(401)
45 | end
46 | end
47 |
48 | describe "PATCH #update" do
49 | let(:new_password) { 'new_password' }
50 | it do
51 | user.generate_password_token!
52 | patch :update, params: { token: user.reset_password_token, password: new_password, password_confirmation: new_password }
53 | expect(response).to be_successful
54 | end
55 |
56 | it 'returns 422 if passwords do not match' do
57 | user.generate_password_token!
58 | patch :update, params: { token: user.reset_password_token, password: new_password, password_confirmation: 1 }
59 | expect(response).to have_http_status(422)
60 | end
61 |
62 | it 'returns 400 if param is missing' do
63 | user.generate_password_token!
64 | patch :update, params: { token: user.reset_password_token, password: new_password }
65 | expect(response).to have_http_status(400)
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/todos-vue/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // Template version: 1.3.1
3 | // see http://vuejs-templates.github.io/webpack for documentation.
4 |
5 | const path = require('path')
6 |
7 | module.exports = {
8 | dev: {
9 |
10 | // Paths
11 | assetsSubDirectory: 'static',
12 | assetsPublicPath: '/',
13 | proxyTable: {},
14 |
15 | // Various Dev Server settings
16 | host: 'localhost', // can be overwritten by process.env.HOST
17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
18 | autoOpenBrowser: false,
19 | errorOverlay: true,
20 | notifyOnErrors: true,
21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
22 |
23 | // Use Eslint Loader?
24 | // If true, your code will be linted during bundling and
25 | // linting errors and warnings will be shown in the console.
26 | useEslint: true,
27 | // If true, eslint errors and warnings will also be shown in the error overlay
28 | // in the browser.
29 | showEslintErrorsInOverlay: false,
30 |
31 | /**
32 | * Source Maps
33 | */
34 |
35 | // https://webpack.js.org/configuration/devtool/#development
36 | devtool: 'cheap-module-eval-source-map',
37 |
38 | // If you have problems debugging vue-files in devtools,
39 | // set this to false - it *may* help
40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting
41 | cacheBusting: true,
42 |
43 | cssSourceMap: true
44 | },
45 |
46 | build: {
47 | // Template for index.html
48 | index: path.resolve(__dirname, '../dist/index.html'),
49 |
50 | // Paths
51 | assetsRoot: path.resolve(__dirname, '../dist'),
52 | assetsSubDirectory: 'static',
53 | assetsPublicPath: '/',
54 |
55 | /**
56 | * Source Maps
57 | */
58 |
59 | productionSourceMap: true,
60 | // https://webpack.js.org/configuration/devtool/#production
61 | devtool: '#source-map',
62 |
63 | // Gzip off by default as many popular static hosts such as
64 | // Surge or Netlify already gzip all static assets for you.
65 | // Before setting to `true`, make sure to:
66 | // npm install --save-dev compression-webpack-plugin
67 | productionGzip: false,
68 | productionGzipExtensions: ['js', 'css'],
69 |
70 | // Run the build command with an extra argument to
71 | // View the bundle analyzer report after build finishes:
72 | // `npm run build --report`
73 | // Set to `true` or `false` to always turn it on or off
74 | bundleAnalyzerReport: process.env.npm_config_report
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/todos-vue/src/components/Signup.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
73 |
--------------------------------------------------------------------------------
/todos-vue/src/components/admin/users/Edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
77 |
--------------------------------------------------------------------------------
/todos-vue/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | const createLintingRule = () => ({
12 | test: /\.(js|vue)$/,
13 | loader: 'eslint-loader',
14 | enforce: 'pre',
15 | include: [resolve('src'), resolve('test')],
16 | options: {
17 | formatter: require('eslint-friendly-formatter'),
18 | emitWarning: !config.dev.showEslintErrorsInOverlay
19 | }
20 | })
21 |
22 | module.exports = {
23 | context: path.resolve(__dirname, '../'),
24 | entry: {
25 | app: './src/main.js'
26 | },
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: '[name].js',
30 | publicPath: process.env.NODE_ENV === 'production'
31 | ? config.build.assetsPublicPath
32 | : config.dev.assetsPublicPath
33 | },
34 | resolve: {
35 | extensions: ['.js', '.vue', '.json'],
36 | alias: {
37 | 'vue$': 'vue/dist/vue.esm.js',
38 | '@': resolve('src'),
39 | }
40 | },
41 | module: {
42 | rules: [
43 | ...(config.dev.useEslint ? [createLintingRule()] : []),
44 | {
45 | test: /\.vue$/,
46 | loader: 'vue-loader',
47 | options: vueLoaderConfig
48 | },
49 | {
50 | test: /\.js$/,
51 | loader: 'babel-loader',
52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
53 | },
54 | {
55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
56 | loader: 'url-loader',
57 | options: {
58 | limit: 10000,
59 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
60 | }
61 | },
62 | {
63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
64 | loader: 'url-loader',
65 | options: {
66 | limit: 10000,
67 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
68 | }
69 | },
70 | {
71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
72 | loader: 'url-loader',
73 | options: {
74 | limit: 10000,
75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
76 | }
77 | }
78 | ]
79 | },
80 | node: {
81 | // prevent webpack from injecting useless setImmediate polyfill because Vue
82 | // source contains it (although only uses it if it's native).
83 | setImmediate: false,
84 | // prevent webpack from injecting mocks to Node native modules
85 | // that does not make sense for the client
86 | dgram: 'empty',
87 | fs: 'empty',
88 | net: 'empty',
89 | tls: 'empty',
90 | child_process: 'empty'
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | require 'spec_helper'
3 | ENV['RAILS_ENV'] ||= 'test'
4 | require File.expand_path('../../config/environment', __FILE__)
5 | # Prevent database truncation if the environment is production
6 | abort("The Rails environment is running in production mode!") if Rails.env.production?
7 | require 'rspec/rails'
8 | # Add additional requires below this line. Rails is not loaded until this point!
9 |
10 | # Requires supporting ruby files with custom matchers and macros, etc, in
11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
12 | # run as spec files by default. This means that files in spec/support that end
13 | # in _spec.rb will both be required and run as specs, causing the specs to be
14 | # run twice. It is recommended that you do not name files matching this glob to
15 | # end with _spec.rb. You can configure this pattern with the --pattern
16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
17 | #
18 | # The following line is provided for convenience purposes. It has the downside
19 | # of increasing the boot-up time by auto-requiring all files in the support
20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
21 | # require only the support files necessary.
22 | #
23 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
24 |
25 | # Checks for pending migrations and applies them before tests are run.
26 | # If you are not using ActiveRecord, you can remove this line.
27 | ActiveRecord::Migration.maintain_test_schema!
28 |
29 | RSpec.configure do |config|
30 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
31 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
32 |
33 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
34 | # examples within a transaction, remove the following line or assign false
35 | # instead of true.
36 | config.use_transactional_fixtures = true
37 | config.include FactoryBot::Syntax::Methods
38 |
39 | # RSpec Rails can automatically mix in different behaviours to your tests
40 | # based on their file location, for example enabling you to call `get` and
41 | # `post` in specs under `spec/controllers`.
42 | #
43 | # You can disable this behaviour by removing the line below, and instead
44 | # explicitly tag your specs with their type, e.g.:
45 | #
46 | # RSpec.describe UsersController, :type => :controller do
47 | # # ...
48 | # end
49 | #
50 | # The different available types are documented in the features, such as in
51 | # https://relishapp.com/rspec/rspec-rails/docs
52 | config.infer_spec_type_from_file_location!
53 |
54 | # Filter lines from Rails gems in backtraces.
55 | config.filter_rails_from_backtrace!
56 | # arbitrary gems may also be filtered via:
57 | # config.filter_gems_from_backtrace("gem name")
58 | end
59 |
--------------------------------------------------------------------------------
/todos-vue/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const packageConfig = require('../package.json')
6 |
7 | exports.assetsPath = function (_path) {
8 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
9 | ? config.build.assetsSubDirectory
10 | : config.dev.assetsSubDirectory
11 |
12 | return path.posix.join(assetsSubDirectory, _path)
13 | }
14 |
15 | exports.cssLoaders = function (options) {
16 | options = options || {}
17 |
18 | const cssLoader = {
19 | loader: 'css-loader',
20 | options: {
21 | sourceMap: options.sourceMap
22 | }
23 | }
24 |
25 | const postcssLoader = {
26 | loader: 'postcss-loader',
27 | options: {
28 | sourceMap: options.sourceMap
29 | }
30 | }
31 |
32 | // generate loader string to be used with extract text plugin
33 | function generateLoaders (loader, loaderOptions) {
34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
35 |
36 | if (loader) {
37 | loaders.push({
38 | loader: loader + '-loader',
39 | options: Object.assign({}, loaderOptions, {
40 | sourceMap: options.sourceMap
41 | })
42 | })
43 | }
44 |
45 | // Extract CSS when that option is specified
46 | // (which is the case during production build)
47 | if (options.extract) {
48 | return ExtractTextPlugin.extract({
49 | use: loaders,
50 | fallback: 'vue-style-loader'
51 | })
52 | } else {
53 | return ['vue-style-loader'].concat(loaders)
54 | }
55 | }
56 |
57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
58 | return {
59 | css: generateLoaders(),
60 | postcss: generateLoaders(),
61 | less: generateLoaders('less'),
62 | sass: generateLoaders('sass', { indentedSyntax: true }),
63 | scss: generateLoaders('sass'),
64 | stylus: generateLoaders('stylus'),
65 | styl: generateLoaders('stylus')
66 | }
67 | }
68 |
69 | // Generate loaders for standalone style files (outside of .vue)
70 | exports.styleLoaders = function (options) {
71 | const output = []
72 | const loaders = exports.cssLoaders(options)
73 |
74 | for (const extension in loaders) {
75 | const loader = loaders[extension]
76 | output.push({
77 | test: new RegExp('\\.' + extension + '$'),
78 | use: loader
79 | })
80 | }
81 |
82 | return output
83 | }
84 |
85 | exports.createNotifierCallback = () => {
86 | const notifier = require('node-notifier')
87 |
88 | return (severity, errors) => {
89 | if (severity !== 'error') return
90 |
91 | const error = errors[0]
92 | const filename = error.file && error.file.split('!').pop()
93 |
94 | notifier.notify({
95 | title: packageConfig.name,
96 | message: severity + ': ' + error.name,
97 | subtitle: filename || '',
98 | icon: path.join(__dirname, 'logo.png')
99 | })
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/todos-vue/src/components/todos/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ error }}
5 |
Todos
6 |
11 |
12 |
27 |
28 |
29 |
30 |
90 |
91 |
105 |
--------------------------------------------------------------------------------
/todos-vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todos-vue",
3 | "version": "1.0.0",
4 | "description": "Todos Vue.js application",
5 | "author": "Yulia Oletskaya ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9 | "start": "npm run dev",
10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
11 | "test": "npm run unit",
12 | "lint": "eslint --ext .js,.vue src test/unit",
13 | "build": "node build/build.js"
14 | },
15 | "dependencies": {
16 | "axios": "^0.18.1",
17 | "vue": "^2.6.10",
18 | "vue-axios": "^2.1.5",
19 | "vue-router": "^3.1.3",
20 | "vuex": "^3.1.1",
21 | "vuex-persistedstate": "^2.5.4"
22 | },
23 | "devDependencies": {
24 | "autoprefixer": "^7.1.2",
25 | "babel-core": "^6.22.1",
26 | "babel-eslint": "^8.2.6",
27 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
28 | "babel-loader": "^7.1.5",
29 | "babel-plugin-istanbul": "^4.1.1",
30 | "babel-plugin-syntax-jsx": "^6.18.0",
31 | "babel-plugin-transform-runtime": "^6.22.0",
32 | "babel-plugin-transform-vue-jsx": "^3.5.0",
33 | "babel-preset-env": "^1.3.2",
34 | "babel-preset-stage-2": "^6.22.0",
35 | "chai": "^4.2.0",
36 | "chalk": "^2.4.2",
37 | "copy-webpack-plugin": "^4.6.0",
38 | "cross-env": "^5.2.1",
39 | "css-loader": "^3.2.0",
40 | "eslint": "^4.15.0",
41 | "eslint-config-standard": "^10.2.1",
42 | "eslint-friendly-formatter": "^3.0.0",
43 | "eslint-loader": "^2.0.0",
44 | "eslint-plugin-import": "^2.18.2",
45 | "eslint-plugin-node": "^5.2.0",
46 | "eslint-plugin-promise": "^3.4.0",
47 | "eslint-plugin-standard": "^3.0.1",
48 | "eslint-plugin-vue": "^4.7.1",
49 | "extract-text-webpack-plugin": "^3.0.2",
50 | "file-loader": "^1.1.4",
51 | "friendly-errors-webpack-plugin": "^1.7.0",
52 | "html-webpack-plugin": "^3.2.0",
53 | "inject-loader": "^3.0.0",
54 | "karma": "^4.4.1",
55 | "karma-coverage": "^1.1.1",
56 | "karma-mocha": "^1.3.0",
57 | "karma-phantomjs-launcher": "^1.0.2",
58 | "karma-phantomjs-shim": "^1.4.0",
59 | "karma-sinon-chai": "^1.3.1",
60 | "karma-sourcemap-loader": "^0.3.7",
61 | "karma-spec-reporter": "0.0.31",
62 | "karma-webpack": "^2.0.2",
63 | "mocha": "^6.2.2",
64 | "node-notifier": "^5.4.3",
65 | "optimize-css-assets-webpack-plugin": "^3.2.1",
66 | "ora": "^1.2.0",
67 | "phantomjs-prebuilt": "^2.1.14",
68 | "portfinder": "^1.0.26",
69 | "postcss-import": "^11.0.0",
70 | "postcss-loader": "^2.1.6",
71 | "postcss-url": "^7.2.1",
72 | "rimraf": "^2.7.1",
73 | "semver": "^5.7.1",
74 | "shelljs": "^0.7.6",
75 | "sinon": "^4.0.0",
76 | "sinon-chai": "^2.8.0",
77 | "uglifyjs-webpack-plugin": "^1.3.0",
78 | "url-loader": "^2.2.0",
79 | "vue-loader": "^14.2.2",
80 | "vue-style-loader": "^3.0.1",
81 | "vue-template-compiler": "^2.6.10",
82 | "webpack": "^4.43.0",
83 | "webpack-bundle-analyzer": "^3.6.0",
84 | "webpack-cli": "^3.3.12",
85 | "webpack-dev-server": "^3.11.0",
86 | "webpack-merge": "^4.2.2"
87 | },
88 | "engines": {
89 | "node": ">= 6.0.0",
90 | "npm": ">= 3.0.0"
91 | },
92 | "browserslist": [
93 | "> 1%",
94 | "last 2 versions",
95 | "not ie <= 8"
96 | ]
97 | }
98 |
--------------------------------------------------------------------------------
/todos-vue/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config')
5 | const merge = require('webpack-merge')
6 | const path = require('path')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
11 | const portfinder = require('portfinder')
12 |
13 | const HOST = process.env.HOST
14 | const PORT = process.env.PORT && Number(process.env.PORT)
15 |
16 | const devWebpackConfig = merge(baseWebpackConfig, {
17 | module: {
18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
19 | },
20 | // cheap-module-eval-source-map is faster for development
21 | devtool: config.dev.devtool,
22 |
23 | // these devServer options should be customized in /config/index.js
24 | devServer: {
25 | clientLogLevel: 'warning',
26 | historyApiFallback: {
27 | rewrites: [
28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
29 | ],
30 | },
31 | hot: true,
32 | contentBase: false, // since we use CopyWebpackPlugin.
33 | compress: true,
34 | host: HOST || config.dev.host,
35 | port: PORT || config.dev.port,
36 | open: config.dev.autoOpenBrowser,
37 | overlay: config.dev.errorOverlay
38 | ? { warnings: false, errors: true }
39 | : false,
40 | publicPath: config.dev.assetsPublicPath,
41 | proxy: config.dev.proxyTable,
42 | quiet: true, // necessary for FriendlyErrorsPlugin
43 | watchOptions: {
44 | poll: config.dev.poll,
45 | }
46 | },
47 | plugins: [
48 | new webpack.DefinePlugin({
49 | 'process.env': require('../config/dev.env')
50 | }),
51 | new webpack.HotModuleReplacementPlugin(),
52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
53 | new webpack.NoEmitOnErrorsPlugin(),
54 | // https://github.com/ampedandwired/html-webpack-plugin
55 | new HtmlWebpackPlugin({
56 | filename: 'index.html',
57 | template: 'index.html',
58 | inject: true
59 | }),
60 | // copy custom static assets
61 | new CopyWebpackPlugin([
62 | {
63 | from: path.resolve(__dirname, '../static'),
64 | to: config.dev.assetsSubDirectory,
65 | ignore: ['.*']
66 | }
67 | ])
68 | ]
69 | })
70 |
71 | module.exports = new Promise((resolve, reject) => {
72 | portfinder.basePort = process.env.PORT || config.dev.port
73 | portfinder.getPort((err, port) => {
74 | if (err) {
75 | reject(err)
76 | } else {
77 | // publish the new Port, necessary for e2e tests
78 | process.env.PORT = port
79 | // add port to devServer config
80 | devWebpackConfig.devServer.port = port
81 |
82 | // Add FriendlyErrorsPlugin
83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
84 | compilationSuccessInfo: {
85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
86 | },
87 | onErrors: config.dev.notifyOnErrors
88 | ? utils.createNotifierCallback()
89 | : undefined
90 | }))
91 |
92 | resolve(devWebpackConfig)
93 | }
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/spec/controllers/todos_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe TodosController, type: :controller do
4 | let(:user) { create(:user) }
5 |
6 | let(:valid_attributes) {
7 | { title: 'new title' }
8 | }
9 |
10 | let(:invalid_attributes) {
11 | { title: nil }
12 | }
13 |
14 | before { sign_in_as(user) }
15 |
16 | describe 'GET #index' do
17 | let!(:todo) { create(:todo, user: user) }
18 |
19 | it 'returns a success response' do
20 | get :index
21 | expect(response).to be_successful
22 | expect(response_json.size).to eq 1
23 | expect(response_json.first['id']).to eq todo.id
24 | end
25 |
26 | # usually there's no need to test this kind of stuff, it's here for the presentation purpose
27 | it 'unauth without cookie' do
28 | request.cookies[JWTSessions.access_cookie] = nil
29 | get :index
30 | expect(response).to have_http_status(401)
31 | end
32 | end
33 |
34 | describe 'GET #show' do
35 | let!(:todo) { create(:todo, user: user) }
36 | before { sign_in_as(user) }
37 |
38 | it 'returns a success response' do
39 | get :show, params: { id: todo.id }
40 | expect(response).to be_successful
41 | end
42 | end
43 |
44 | describe 'POST #create' do
45 |
46 | context 'with valid params' do
47 | it 'creates a new Todo' do
48 | expect {
49 | post :create, params: { todo: valid_attributes }
50 | }.to change(Todo, :count).by(1)
51 | end
52 |
53 | it 'renders a JSON response with the new todo' do
54 | post :create, params: { todo: valid_attributes }
55 | expect(response).to have_http_status(:created)
56 | expect(response.content_type).to eq('application/json')
57 | expect(response.location).to eq(todo_url(Todo.last))
58 | end
59 |
60 | it 'unauth without CSRF' do
61 | request.headers[JWTSessions.csrf_header] = nil
62 | post :create, params: { todo: valid_attributes }
63 | expect(response).to have_http_status(401)
64 | end
65 | end
66 |
67 | context 'with invalid params' do
68 | it 'renders a JSON response with errors for the new todo' do
69 | post :create, params: { todo: invalid_attributes }
70 | expect(response).to have_http_status(:unprocessable_entity)
71 | expect(response.content_type).to eq('application/json')
72 | end
73 | end
74 | end
75 |
76 | describe 'PUT #update' do
77 | let!(:todo) { create(:todo, user: user) }
78 |
79 | context 'with valid params' do
80 | let(:new_attributes) {
81 | { title: 'Super secret title' }
82 | }
83 |
84 | it 'updates the requested todo' do
85 | put :update, params: { id: todo.id, todo: new_attributes }
86 | todo.reload
87 | expect(todo.title).to eq new_attributes[:title]
88 | end
89 |
90 | it 'renders a JSON response with the todo' do
91 | put :update, params: { id: todo.to_param, todo: valid_attributes }
92 | expect(response).to have_http_status(:ok)
93 | expect(response.content_type).to eq('application/json')
94 | end
95 | end
96 |
97 | context 'with invalid params' do
98 | it 'renders a JSON response with errors for the todo' do
99 | put :update, params: { id: todo.to_param, todo: invalid_attributes }
100 | expect(response).to have_http_status(:unprocessable_entity)
101 | expect(response.content_type).to eq('application/json')
102 | end
103 | end
104 | end
105 |
106 | describe 'DELETE #destroy' do
107 | let!(:todo) { create(:todo, user: user) }
108 |
109 | it 'destroys the requested todo' do
110 | expect {
111 | delete :destroy, params: { id: todo.id }
112 | }.to change(Todo, :count).by(-1)
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
19 | # config.require_master_key = true
20 |
21 | # Disable serving static files from the `/public` folder by default since
22 | # Apache or NGINX already handles this.
23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
24 |
25 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
26 | # config.action_controller.asset_host = 'http://assets.example.com'
27 |
28 | # Specifies the header that your server uses for sending files.
29 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
30 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
31 |
32 | # Store uploaded files on the local file system (see config/storage.yml for options)
33 | config.active_storage.service = :local
34 |
35 | # Mount Action Cable outside main process or domain
36 | # config.action_cable.mount_path = nil
37 | # config.action_cable.url = 'wss://example.com/cable'
38 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
39 |
40 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
41 | # config.force_ssl = true
42 |
43 | # Use the lowest log level to ensure availability of diagnostic information
44 | # when problems arise.
45 | config.log_level = :debug
46 |
47 | # Prepend all log lines with the following tags.
48 | config.log_tags = [ :request_id ]
49 |
50 | # Use a different cache store in production.
51 | # config.cache_store = :mem_cache_store
52 |
53 | # Use a real queuing backend for Active Job (and separate queues per environment)
54 | # config.active_job.queue_adapter = :resque
55 | # config.active_job.queue_name_prefix = "silver-octo-invention_#{Rails.env}"
56 |
57 | config.action_mailer.perform_caching = false
58 |
59 | # Ignore bad email addresses and do not raise email delivery errors.
60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
61 | # config.action_mailer.raise_delivery_errors = false
62 |
63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
64 | # the I18n.default_locale when a translation cannot be found).
65 | config.i18n.fallbacks = true
66 |
67 | # Send deprecation notices to registered listeners.
68 | config.active_support.deprecation = :notify
69 |
70 | # Use default logging formatter so that PID and timestamp are not suppressed.
71 | config.log_formatter = ::Logger::Formatter.new
72 |
73 | # Use a different logger for distributed setups.
74 | # require 'syslog/logger'
75 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
76 |
77 | if ENV["RAILS_LOG_TO_STDOUT"].present?
78 | logger = ActiveSupport::Logger.new(STDOUT)
79 | logger.formatter = config.log_formatter
80 | config.logger = ActiveSupport::TaggedLogging.new(logger)
81 | end
82 |
83 | # Do not dump schema after migrations.
84 | config.active_record.dump_schema_after_migration = false
85 | end
86 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause
4 | # this file to always be loaded, without a need to explicitly require it in any
5 | # files.
6 | #
7 | # Given that it is always loaded, you are encouraged to keep this file as
8 | # light-weight as possible. Requiring heavyweight dependencies from this file
9 | # will add to the boot time of your test suite on EVERY test run, even for an
10 | # individual file that may not need all of that loaded. Instead, consider making
11 | # a separate helper file that requires the additional dependencies and performs
12 | # the additional setup, and require it from the spec files that actually need
13 | # it.
14 | #
15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16 |
17 | require_relative 'support/auth_helper'
18 | require_relative 'support/response_helper'
19 |
20 | RSpec.configure do |config|
21 | # rspec-expectations config goes here. You can use an alternate
22 | # assertion/expectation library such as wrong or the stdlib/minitest
23 | # assertions if you prefer.
24 | config.expect_with :rspec do |expectations|
25 | # This option will default to `true` in RSpec 4. It makes the `description`
26 | # and `failure_message` of custom matchers include text for helper methods
27 | # defined using `chain`, e.g.:
28 | # be_bigger_than(2).and_smaller_than(4).description
29 | # # => "be bigger than 2 and smaller than 4"
30 | # ...rather than:
31 | # # => "be bigger than 2"
32 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
33 | end
34 |
35 | # rspec-mocks config goes here. You can use an alternate test double
36 | # library (such as bogus or mocha) by changing the `mock_with` option here.
37 | config.mock_with :rspec do |mocks|
38 | # Prevents you from mocking or stubbing a method that does not exist on
39 | # a real object. This is generally recommended, and will default to
40 | # `true` in RSpec 4.
41 | mocks.verify_partial_doubles = true
42 | end
43 |
44 | config.include ResponseHelper
45 | config.include AuthHelper
46 |
47 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
48 | # have no way to turn it off -- the option exists only for backwards
49 | # compatibility in RSpec 3). It causes shared context metadata to be
50 | # inherited by the metadata hash of host groups and examples, rather than
51 | # triggering implicit auto-inclusion in groups with matching metadata.
52 | config.shared_context_metadata_behavior = :apply_to_host_groups
53 |
54 | # The settings below are suggested to provide a good initial experience
55 | # with RSpec, but feel free to customize to your heart's content.
56 | =begin
57 | # This allows you to limit a spec run to individual examples or groups
58 | # you care about by tagging them with `:focus` metadata. When nothing
59 | # is tagged with `:focus`, all examples get run. RSpec also provides
60 | # aliases for `it`, `describe`, and `context` that include `:focus`
61 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
62 | config.filter_run_when_matching :focus
63 |
64 | # Allows RSpec to persist some state between runs in order to support
65 | # the `--only-failures` and `--next-failure` CLI options. We recommend
66 | # you configure your source control system to ignore this file.
67 | config.example_status_persistence_file_path = "spec/examples.txt"
68 |
69 | # Limits the available syntax to the non-monkey patched syntax that is
70 | # recommended. For more details, see:
71 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
72 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
73 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
74 | config.disable_monkey_patching!
75 |
76 | # Many RSpec users commonly either run the entire suite or an individual
77 | # file, and it's useful to allow more verbose output when running an
78 | # individual spec file.
79 | if config.files_to_run.one?
80 | # Use the documentation formatter for detailed output,
81 | # unless a formatter has already been configured
82 | # (e.g. via a command-line flag).
83 | config.default_formatter = "doc"
84 | end
85 |
86 | # Print the 10 slowest examples and example groups at the
87 | # end of the spec run, to help surface which specs are running
88 | # particularly slow.
89 | config.profile_examples = 10
90 |
91 | # Run specs in random order to surface order dependencies. If you find an
92 | # order dependency and want to debug it, you can fix the order by providing
93 | # the seed, which is printed after each run.
94 | # --seed 1234
95 | config.order = :random
96 |
97 | # Seed global randomization in this process using the `--seed` CLI option.
98 | # Setting this allows you to use `--seed` to deterministically reproduce
99 | # test failures related to randomization by passing the same `--seed` value
100 | # as the one that triggered the failure.
101 | Kernel.srand config.seed
102 | =end
103 | end
104 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.2.3)
5 | actionpack (= 5.2.3)
6 | nio4r (~> 2.0)
7 | websocket-driver (>= 0.6.1)
8 | actionmailer (5.2.3)
9 | actionpack (= 5.2.3)
10 | actionview (= 5.2.3)
11 | activejob (= 5.2.3)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 2.0)
14 | actionpack (5.2.3)
15 | actionview (= 5.2.3)
16 | activesupport (= 5.2.3)
17 | rack (~> 2.0)
18 | rack-test (>= 0.6.3)
19 | rails-dom-testing (~> 2.0)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.2.3)
22 | activesupport (= 5.2.3)
23 | builder (~> 3.1)
24 | erubi (~> 1.4)
25 | rails-dom-testing (~> 2.0)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
27 | activejob (5.2.3)
28 | activesupport (= 5.2.3)
29 | globalid (>= 0.3.6)
30 | activemodel (5.2.3)
31 | activesupport (= 5.2.3)
32 | activerecord (5.2.3)
33 | activemodel (= 5.2.3)
34 | activesupport (= 5.2.3)
35 | arel (>= 9.0)
36 | activestorage (5.2.3)
37 | actionpack (= 5.2.3)
38 | activerecord (= 5.2.3)
39 | marcel (~> 0.3.1)
40 | activesupport (5.2.3)
41 | concurrent-ruby (~> 1.0, >= 1.0.2)
42 | i18n (>= 0.7, < 2)
43 | minitest (~> 5.1)
44 | tzinfo (~> 1.1)
45 | arel (9.0.0)
46 | bcrypt (3.1.13)
47 | bootsnap (1.4.5)
48 | msgpack (~> 1.0)
49 | builder (3.2.3)
50 | byebug (11.0.1)
51 | coderay (1.1.2)
52 | concurrent-ruby (1.1.5)
53 | crass (1.0.5)
54 | diff-lcs (1.3)
55 | erubi (1.9.0)
56 | factory_bot (4.11.1)
57 | activesupport (>= 3.0.0)
58 | factory_bot_rails (4.11.1)
59 | factory_bot (~> 4.11.1)
60 | railties (>= 3.0.0)
61 | ffi (1.11.1)
62 | globalid (0.4.2)
63 | activesupport (>= 4.2.0)
64 | i18n (1.7.0)
65 | concurrent-ruby (~> 1.0)
66 | jwt (2.2.1)
67 | jwt_sessions (2.4.3)
68 | jwt (>= 2.1.1, < 3)
69 | listen (3.1.5)
70 | rb-fsevent (~> 0.9, >= 0.9.4)
71 | rb-inotify (~> 0.9, >= 0.9.7)
72 | ruby_dep (~> 1.2)
73 | loofah (2.3.1)
74 | crass (~> 1.0.2)
75 | nokogiri (>= 1.5.9)
76 | mail (2.7.1)
77 | mini_mime (>= 0.1.1)
78 | marcel (0.3.3)
79 | mimemagic (~> 0.3.2)
80 | method_source (0.9.2)
81 | mimemagic (0.3.3)
82 | mini_mime (1.0.2)
83 | mini_portile2 (2.4.0)
84 | minitest (5.12.2)
85 | msgpack (1.3.1)
86 | nio4r (2.5.2)
87 | nokogiri (1.10.8)
88 | mini_portile2 (~> 2.4.0)
89 | pry (0.12.2)
90 | coderay (~> 1.1.0)
91 | method_source (~> 0.9.0)
92 | pry-byebug (3.7.0)
93 | byebug (~> 11.0)
94 | pry (~> 0.10)
95 | pry-rails (0.3.9)
96 | pry (>= 0.10.4)
97 | puma (3.12.6)
98 | rack (2.2.3)
99 | rack-cors (1.0.5)
100 | rack (>= 1.6.0)
101 | rack-test (1.1.0)
102 | rack (>= 1.0, < 3)
103 | rails (5.2.3)
104 | actioncable (= 5.2.3)
105 | actionmailer (= 5.2.3)
106 | actionpack (= 5.2.3)
107 | actionview (= 5.2.3)
108 | activejob (= 5.2.3)
109 | activemodel (= 5.2.3)
110 | activerecord (= 5.2.3)
111 | activestorage (= 5.2.3)
112 | activesupport (= 5.2.3)
113 | bundler (>= 1.3.0)
114 | railties (= 5.2.3)
115 | sprockets-rails (>= 2.0.0)
116 | rails-dom-testing (2.0.3)
117 | activesupport (>= 4.2.0)
118 | nokogiri (>= 1.6)
119 | rails-html-sanitizer (1.3.0)
120 | loofah (~> 2.3)
121 | railties (5.2.3)
122 | actionpack (= 5.2.3)
123 | activesupport (= 5.2.3)
124 | method_source
125 | rake (>= 0.8.7)
126 | thor (>= 0.19.0, < 2.0)
127 | rake (13.0.0)
128 | rb-fsevent (0.10.3)
129 | rb-inotify (0.10.0)
130 | ffi (~> 1.0)
131 | redis (4.1.3)
132 | rspec-core (3.9.0)
133 | rspec-support (~> 3.9.0)
134 | rspec-expectations (3.9.0)
135 | diff-lcs (>= 1.2.0, < 2.0)
136 | rspec-support (~> 3.9.0)
137 | rspec-mocks (3.9.0)
138 | diff-lcs (>= 1.2.0, < 2.0)
139 | rspec-support (~> 3.9.0)
140 | rspec-rails (3.9.0)
141 | actionpack (>= 3.0)
142 | activesupport (>= 3.0)
143 | railties (>= 3.0)
144 | rspec-core (~> 3.9.0)
145 | rspec-expectations (~> 3.9.0)
146 | rspec-mocks (~> 3.9.0)
147 | rspec-support (~> 3.9.0)
148 | rspec-support (3.9.0)
149 | ruby_dep (1.5.0)
150 | spring (2.1.0)
151 | spring-watcher-listen (2.0.1)
152 | listen (>= 2.7, < 4.0)
153 | spring (>= 1.2, < 3.0)
154 | sprockets (3.7.2)
155 | concurrent-ruby (~> 1.0)
156 | rack (> 1, < 3)
157 | sprockets-rails (3.2.1)
158 | actionpack (>= 4.0)
159 | activesupport (>= 4.0)
160 | sprockets (>= 3.0.0)
161 | sqlite3 (1.4.1)
162 | thor (0.20.3)
163 | thread_safe (0.3.6)
164 | tzinfo (1.2.5)
165 | thread_safe (~> 0.1)
166 | websocket-driver (0.7.1)
167 | websocket-extensions (>= 0.1.0)
168 | websocket-extensions (0.1.5)
169 |
170 | PLATFORMS
171 | ruby
172 |
173 | DEPENDENCIES
174 | bcrypt (~> 3.1.7)
175 | bootsnap (>= 1.1.0)
176 | factory_bot_rails (~> 4.8)
177 | jwt_sessions (~> 2.4)
178 | listen (>= 3.0.5, < 3.2)
179 | pry-byebug (~> 3.4)
180 | pry-rails (~> 0.3.4)
181 | puma (~> 3.12)
182 | rack-cors
183 | rails (~> 5.2.0)
184 | redis (~> 4.0)
185 | rspec-rails (~> 3.7)
186 | spring
187 | spring-watcher-listen (~> 2.0.0)
188 | sqlite3
189 |
190 | RUBY VERSION
191 | ruby 2.4.4p296
192 |
193 | BUNDLED WITH
194 | 1.16.1
195 |
--------------------------------------------------------------------------------
/todos-vue/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13 |
14 | const env = process.env.NODE_ENV === 'testing'
15 | ? require('../config/test.env')
16 | : require('../config/prod.env')
17 |
18 | const webpackConfig = merge(baseWebpackConfig, {
19 | module: {
20 | rules: utils.styleLoaders({
21 | sourceMap: config.build.productionSourceMap,
22 | extract: true,
23 | usePostCSS: true
24 | })
25 | },
26 | devtool: config.build.productionSourceMap ? config.build.devtool : false,
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
31 | },
32 | plugins: [
33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
34 | new webpack.DefinePlugin({
35 | 'process.env': env
36 | }),
37 | new UglifyJsPlugin({
38 | uglifyOptions: {
39 | compress: {
40 | warnings: false
41 | }
42 | },
43 | sourceMap: config.build.productionSourceMap,
44 | parallel: true
45 | }),
46 | // extract css into its own file
47 | new ExtractTextPlugin({
48 | filename: utils.assetsPath('css/[name].[contenthash].css'),
49 | // Setting the following option to `false` will not extract CSS from codesplit chunks.
50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
53 | allChunks: true,
54 | }),
55 | // Compress extracted CSS. We are using this plugin so that possible
56 | // duplicated CSS from different components can be deduped.
57 | new OptimizeCSSPlugin({
58 | cssProcessorOptions: config.build.productionSourceMap
59 | ? { safe: true, map: { inline: false } }
60 | : { safe: true }
61 | }),
62 | // generate dist index.html with correct asset hash for caching.
63 | // you can customize output by editing /index.html
64 | // see https://github.com/ampedandwired/html-webpack-plugin
65 | new HtmlWebpackPlugin({
66 | filename: process.env.NODE_ENV === 'testing'
67 | ? 'index.html'
68 | : config.build.index,
69 | template: 'index.html',
70 | inject: true,
71 | minify: {
72 | removeComments: true,
73 | collapseWhitespace: true,
74 | removeAttributeQuotes: true
75 | // more options:
76 | // https://github.com/kangax/html-minifier#options-quick-reference
77 | },
78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
79 | chunksSortMode: 'dependency'
80 | }),
81 | // keep module.id stable when vendor modules does not change
82 | new webpack.HashedModuleIdsPlugin(),
83 | // enable scope hoisting
84 | new webpack.optimize.ModuleConcatenationPlugin(),
85 | // split vendor js into its own file
86 | new webpack.optimize.CommonsChunkPlugin({
87 | name: 'vendor',
88 | minChunks (module) {
89 | // any required modules inside node_modules are extracted to vendor
90 | return (
91 | module.resource &&
92 | /\.js$/.test(module.resource) &&
93 | module.resource.indexOf(
94 | path.join(__dirname, '../node_modules')
95 | ) === 0
96 | )
97 | }
98 | }),
99 | // extract webpack runtime and module manifest to its own file in order to
100 | // prevent vendor hash from being updated whenever app bundle is updated
101 | new webpack.optimize.CommonsChunkPlugin({
102 | name: 'manifest',
103 | minChunks: Infinity
104 | }),
105 | // This instance extracts shared chunks from code splitted chunks and bundles them
106 | // in a separate chunk, similar to the vendor chunk
107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
108 | new webpack.optimize.CommonsChunkPlugin({
109 | name: 'app',
110 | async: 'vendor-async',
111 | children: true,
112 | minChunks: 3
113 | }),
114 |
115 | // copy custom static assets
116 | new CopyWebpackPlugin([
117 | {
118 | from: path.resolve(__dirname, '../static'),
119 | to: config.build.assetsSubDirectory,
120 | ignore: ['.*']
121 | }
122 | ])
123 | ]
124 | })
125 |
126 | if (config.build.productionGzip) {
127 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
128 |
129 | webpackConfig.plugins.push(
130 | new CompressionWebpackPlugin({
131 | asset: '[path].gz[query]',
132 | algorithm: 'gzip',
133 | test: new RegExp(
134 | '\\.(' +
135 | config.build.productionGzipExtensions.join('|') +
136 | ')$'
137 | ),
138 | threshold: 10240,
139 | minRatio: 0.8
140 | })
141 | )
142 | }
143 |
144 | if (config.build.bundleAnalyzerReport) {
145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
147 | }
148 |
149 | module.exports = webpackConfig
150 |
--------------------------------------------------------------------------------