├── 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 | 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 | 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 | 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 | 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 | 19 | 20 | 60 | -------------------------------------------------------------------------------- /todos-vue/src/components/Signin.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 68 | -------------------------------------------------------------------------------- /todos-vue/src/components/admin/users/List.vue: -------------------------------------------------------------------------------- 1 | 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 | 24 | 25 | 73 | -------------------------------------------------------------------------------- /todos-vue/src/components/admin/users/Edit.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | --------------------------------------------------------------------------------