Authentication
require 'jwt' module AuthToken def AuthToken.issue_token(payload) payload[:exp] = Time.now.to_i + 4 * 3600 JWT.encode payload, Rails.application.secrets.secret_key_base end def AuthToken.valid?(token) begin JWT.decode token, Rails.application.secrets.secret_key_base rescue false end end end
class AuthController < ApplicationController require 'auth_token' layout false def register @user = User.new(user_params) if @user.save @token = AuthToken.issue_token({ user_id: @user.id }) else render json: { errors: @user.errors }, status: :unauthorized end end def authenticate @user = User.find_by(email: params[:email].downcase) if @user && @user.authenticate(params[:password]) @token = AuthToken.issue_token({ user_id: @user.id }) else render json: { error: "Invalid email/password combination" }, status: :unauthorized end end def token_status token = params[:token] if AuthToken.valid? token head 200 else head 401 end end private def user_params params.permit(:first_name, :last_name, :email, :password) end end
@topTalApp.factory('AuthInterceptor', ($location, $rootScope, $q, $injector) -> authInterceptor = { request: (config) -> token = undefined if localStorage.getItem('auth_token') token = angular.fromJson(localStorage.getItem('auth_token')).token if token config.headers.Authorization = 'Bearer ' + token config responseError: (response) -> if response.status == 401 localStorage.removeItem 'auth_token' $rootScope.errorMsg = response.data.error $location.path '/login' if response.status == 403 $rootScope.errorMsg = response.data.error $location.path '/' $q.reject response } authInterceptor ).config ($httpProvider) -> $httpProvider.interceptors.push 'AuthInterceptor' return
module Api class BaseController < ApplicationController require 'auth_token' before_action :authenticate # jbuilder needs this layout false private def authenticate begin token = request.headers['Authorization'].split(' ').last payload, header = AuthToken.valid?(token) @current_user = User.find_by(id: payload['user_id']) rescue render json: { error: 'You need to login or signup first' }, status: :unauthorized end end end end
Rails.application.routes.draw do root 'home#index' namespace :api, defaults: {format: :json} do resources :users, except: [:new, :edit] resources :expenses, except: [:new, :edit] do collection do get :weekly end end end post '/auth/register', to: 'auth#register', defaults: {format: :json} post '/auth/authenticate', to: 'auth#authenticate', defaults: {format: :json} get '/auth/token_status', to: 'auth#token_status', defaults: {format: :json} end
Authoriza
tionclass User < ActiveRecord::Base has_secure_password validates :email, :password_digest, presence: true validates :email, uniqueness: { case_sensitive: false } validates :password, length: { minimum: 5 } has_many :expenses belongs_to :role ROLES = { REGULAR: 1, ADMIN: 2, MANAGER: 3 } def is_regular self.id_role == ROLES[:REGULAR] end def is_admin self.id_role == ROLES[:ADMIN] end def is_manager self.id_role == ROLES[:MANAGER] end end
- Api
module Api class UsersController < Api::BaseController before_action :is_authorized? private def is_authorized? if @current_user.is_regular render json: { error: "Doesn't have permissions" }, status: :forbidden return end end end end
- Angular
@topTalApp.factory 'Auth', ['$http', 'CurrentUser', 'ROLES', ($http, CurrentUser, ROLES) -> currentUser = CurrentUser token = localStorage.getItem('auth_token') { isRegularUser: (user) -> user.getRole() == ROLES.REGULAR isAdminUser: (user) -> user.getRole() == ROLES.ADMIN isManagerUser: (user) -> user.getRole() == ROLES.MANAGER isAuthorized: (permissions) -> i = 0 while i < permissions.length switch permissions[i] when 'REGULAR' return true if currentUser.getRole() == ROLES.REGULAR when 'ADMIN' return true if currentUser.getRole() == ROLES.ADMIN when 'MANAGER' return true if currentUser.getRole() == ROLES.MANAGER i += 1 return false } ]
@topTalApp = angular.module('topTalApp', ['ngRoute', 'rails', 'templates', 'ui.bootstrap', 'sy.bootstrap.timepicker', 'angularUtils.directives.dirPagination']) @topTalApp .run [ '$rootScope' '$location' 'Auth' ($rootScope, $location, Auth) -> $rootScope.$on '$routeChangeStart', (event, next, current) -> if next.access != undefined and !Auth.isAuthorized(next.access.requiredPermissionsAnyOf) if next.templateUrl == 'expenses/expenses.html' and Auth.isAuthenticated() != null $location.path '/users' else if next.templateUrl == 'expenses/expenses.html' and Auth.isAuthenticated() == null $location.path '/login' else $location.path '/' return return ] .config( ($routeProvider) -> $routeProvider .when '/signup', {templateUrl: 'sessions/signup.html', controller: 'SignupCtrl'} .when '/login', {templateUrl: 'sessions/login.html', controller: 'LoginCtrl'} .when '/', { templateUrl: 'expenses/expenses.html', controller: 'ExpenseCtrl', access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ] } .when '/expenses', { templateUrl: 'expenses/expenses.html', controller: 'ExpenseCtrl', access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ] } .when '/new_expense', { templateUrl: 'expenses/new_edit_expense.html', controller: 'ExpenseCtrl', access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ] } .when '/expense/:id', { templateUrl: 'expenses/new_edit_expense.html', controller: 'ExpenseCtrl', access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ] } .when '/expenses/weekly', { templateUrl: 'expenses/weekly.html', controller: 'ExpenseWeeklyCtrl', access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ] } .when '/users', { templateUrl: 'users/users.html', controller: 'UserCtrl', access: requiredPermissionsAnyOf: [ 'MANAGER', 'ADMIN' ] } .when '/new_user', { templateUrl: 'users/new_edit_user.html', controller: 'UserCtrl', access: requiredPermissionsAnyOf: [ 'MANAGER', 'ADMIN' ] } .when '/user/:id', { templateUrl: 'users/new_edit_user.html', controller: 'UserCtrl', access: requiredPermissionsAnyOf: [ 'MANAGER', 'ADMIN' ] } .otherwise({redirectTo: '/'}) )
Rails API
Angular app
Timezone
def weekly expenses = findByDateFromAndTo params[:datefrom], params[:dateto], :asc my_json = {} add_minutes = - params[:timezone].to_i.minutes expenses.each do |expense| year, month, week = (expense.for_timeday + add_minutes).strftime('%Y'), (expense.for_timeday + add_minutes).strftime('%m'), (expense.for_timeday + add_minutes).strftime('%V') year = (year.to_i + 1).to_s if month.to_i == 12 && week.to_i == 1 key = year + '.' + week analytics = my_json[key] || {} analytics[:expense] = analytics[:expense] || [] analytics[:total] = expense.amount + (analytics[:total] || 0) analytics[:items] = 1 + (analytics[:items] || 0) analytics[:start] = Date.commercial(year.to_i, week.to_i, 1).to_s analytics[:end] = Date.commercial(year.to_i, week.to_i, 7).to_s my_expense = {for_timeday: expense.for_timeday, amount: expense.amount, description: expense.description, comment: expense.comment} analytics[:expense].push my_expense my_json[key] = analytics end render json: my_json.values end
TestS
FactoryGirl.define do factory :expense do association :user, factory: :user_regular amount { Faker::Commerce.price } for_timeday { Faker::Time.between(100.days.ago, 1.day.ago) } description { Faker::Lorem.sentence } comment Faker::Lorem.sentence trait :future_time do for_timeday { Faker::Time.forward(5, :all) } end factory :expense_future, traits: [:future_time] end end
require 'rails_helper' describe Expense do it 'has a valid factory' do expect(build(:expense)).to be_valid end it 'is invalid without an amount' do expect(build(:expense, amount: nil)).to_not be_valid end it 'is invalid without a description' do expect(build(:expense, description: nil)).to_not be_valid end it 'is invalid without a time' do expect(build(:expense, for_timeday: nil)).to_not be_valid end it 'is invalid with a time set in the future' do expect(build(:expense_future)).to_not be_valid end it 'should belong to a user' do expense = build(:expense) user = build(:user) expense.user = user expect(expense.user).to be user end end
require 'rails_helper' describe AuthController do # because of the jBuilder I need to render views render_views describe 'POST #register' do context 'with valid credentials' do it 'returns user id' do #build a user but does not save it into the database user = build(:user_regular) post :register, { email: user.email, password: user.password, format: :json } expect(response.status).to eq 200 parsed_response = JSON.parse response.body expect(parsed_response['user']['id']).to_not be_nil end end context 'with invalid credentials' do it 'does not have email' do post :register, { password: "pass", format: :json } expect(response.status).to eq 401 parsed_response = JSON.parse response.body expect(parsed_response['errors']).to_not be_nil expect(parsed_response['errors']['email'][0]).to eq "can't be blank" end it 'does not have password' do post :register, { email: "[email protected]", format: :json } expect(response.status).to eq 401 parsed_response = JSON.parse response.body expect(parsed_response['errors']).to_not be_nil expect(parsed_response['errors']['password'][0]).to eq "can't be blank" end end end describe 'POST #authenticate' do context 'with valid credentials' do it 'returns token' do user = create(:user_regular) post :authenticate, { email: user.email, password: user.password, format: :json } expect(response.status).to eq 200 parsed_response = JSON.parse response.body expect(parsed_response['token']).to_not be_nil end it 'returns token with 3 parts separated by comas' do user = create(:user_regular) post :authenticate, { email: user.email, password: user.password, format: :json } expect(response.status).to eq 200 parsed_response = JSON.parse response.body expect(parsed_response['token'].split('.').count).to eq 3 end it 'returns first name and last name of the user' do user = create(:user_regular) post :authenticate, { email: user.email, password: user.password, format: :json } expect(response.status).to eq 200 parsed_response = JSON.parse response.body expect(parsed_response['user']['first_name']).to eq user.first_name expect(parsed_response['user']['last_name']).to eq user.last_name end end context 'with invalid credentials' do it 'does not return token' do user = create(:user_regular) post :authenticate, { email: "no_" + user.email, password: user.password, format: :json } expect(response.status).to eq 401 end end end describe 'POST #token_status' do context 'with valid token' do it 'returns OK code' do user = create(:user_regular) token = AuthToken.issue_token({ user_id: user.id }) post :token_status, { token: token, format: :json } expect(response.status).to eq 200 end end end end