From 7d17849214cad355c2d5c77f72386ceabf37b8fc Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 17 Aug 2015 15:25:41 +0200 Subject: [PATCH] Init version of device logging. --- Gemfile | 1 + .../controllers/_profile/devices.js.coffee | 51 ++++++++++++++ .../app/views/profile/devices.jst.eco | 30 ++++++++ app/controllers/application_controller.rb | 22 +++++- app/models/user_device.rb | 69 +++++++++++++++++++ config/routes/user_devices.rb | 8 +++ .../20150817000001_create_user_devices.rb | 22 ++++++ lib/models.rb | 3 + 8 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/app/controllers/_profile/devices.js.coffee create mode 100644 app/assets/javascripts/app/views/profile/devices.jst.eco create mode 100644 app/models/user_device.rb create mode 100644 config/routes/user_devices.rb create mode 100644 db/migrate/20150817000001_create_user_devices.rb diff --git a/Gemfile b/Gemfile index 537856bf0..2474d3fcd 100644 --- a/Gemfile +++ b/Gemfile @@ -55,6 +55,7 @@ gem 'net-ldap' gem 'writeexcel' gem 'icalendar' +gem 'browser' # event machine gem 'eventmachine' diff --git a/app/assets/javascripts/app/controllers/_profile/devices.js.coffee b/app/assets/javascripts/app/controllers/_profile/devices.js.coffee new file mode 100644 index 000000000..b5fcba939 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_profile/devices.js.coffee @@ -0,0 +1,51 @@ +class Index extends App.Controller + events: + 'click [data-type=delete]': 'delete' + + constructor: -> + super + return if !@authenticate() + @title 'Devices', true + + @load() + @interval( + => + @load() + 62000 + ) + + # fetch data, render view + load: => + @ajax( + id: 'user_devices' + type: 'GET' + url: @apiPath + '/user_devices' + success: (data) => + @render(data) + ) + + render: (data) => + @html App.view('profile/devices')( devices: data ) + + delete: (e) => + e.preventDefault() + id = $(e.target).closest('a').data('device-id') + console.log('ID', id) + # get data + @ajax( + id: 'user_devices_delete' + type: 'DELETE' + url: "#{@apiPath}/user_devices/#{id}" + processData: true + success: @load + error: @error + ) + + error: (xhr, status, error) => + data = JSON.parse( xhr.responseText ) + @notify( + type: 'error' + msg: App.i18n.translateContent( data.message ) + ) + +App.Config.set( 'Devices', { prio: 3100, name: 'Devices', parent: '#profile', target: '#profile/devices', controller: Index }, 'NavBarProfile' ) diff --git a/app/assets/javascripts/app/views/profile/devices.jst.eco b/app/assets/javascripts/app/views/profile/devices.jst.eco new file mode 100644 index 000000000..c99ad563d --- /dev/null +++ b/app/assets/javascripts/app/views/profile/devices.jst.eco @@ -0,0 +1,30 @@ + + +
+ +

<%- @T('All computers and browsers that have access to your Zammad appear here.') %>

+ + + + + + + + + + + + <% for device in @devices: %> + + + + + + + <% end %> + +
<%- @T('Name') %><%- @T('Location') %><%- @T('Most recent activity') %><%- @T('Remove') %>
<%= device.name %><%= device.location %><%- @humanTime(device.updated_at) %>
+ +
\ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 691b1197f..26b5029df 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,7 +14,7 @@ class ApplicationController < ActionController::Base :model_index_render skip_before_action :verify_authenticity_token - before_action :set_user, :session_update + before_action :set_user, :session_update, :check_user_device before_action :cors_preflight_check after_action :set_access_control_headers @@ -95,6 +95,26 @@ class ApplicationController < ActionController::Base session[:user_agent] = request.env['HTTP_USER_AGENT'] end + # check user device + def check_user_device + + # only if user_id exists + return if !session[:user_id] + + # only if write action + return if request.method == 'GET' || request.method == 'OPTIONS' + + # only update if needed + return if session[:check_user_device_at] && session[:check_user_device_at] < Time.zone.now - 10.minutes + session[:check_user_device_at] = Time.zone.now + + UserDevice.add( + session[:user_agent], + session[:remote_id], + session[:user_id], + ) + end + def authentication_check_only(auth_param) logger.debug 'authentication_check' diff --git a/app/models/user_device.rb b/app/models/user_device.rb new file mode 100644 index 000000000..6c3ea4d97 --- /dev/null +++ b/app/models/user_device.rb @@ -0,0 +1,69 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class UserDevice < ApplicationModel + store :device_details + store :location_details + validates :name, presence: true + +=begin + +store device for user + + UserDevice.add( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36', + '172.0.0.1', + user.id, + ) + +=end + + def self.add(user_agent, ip, user_id) + + # get browser details + browser = Browser.new(:ua => user_agent, :accept_language => 'en-us') + browser = { + plattform: browser.platform.to_s.camelize, + name: browser.name, + version: browser.version, + full_version: browser.full_version, + } + + # generate device name + name = browser[:plattform] || '' + if browser[:name] + if name + name += ', ' + end + name += browser[:name] + end + + # get location info + location = Service::GeoIp.location(ip) + country = location['country_name'] + + # check if exists + exists = self.find_by( + :user_id => user_id, + os: browser[:plattform], + browser: browser[:name], + country: country, + ) + + if exists + exists.touch + return exists + end + + # create new device + self.create( + user_id: user_id, + name: name, + os: browser[:plattform], + browser: browser[:name], + country: country, + device_details: browser, + location_details: location, + ) + end + +end diff --git a/config/routes/user_devices.rb b/config/routes/user_devices.rb new file mode 100644 index 000000000..ba44cdbba --- /dev/null +++ b/config/routes/user_devices.rb @@ -0,0 +1,8 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + # jobs + match api_path + '/user_devices', to: 'user_devices#index', via: :get + match api_path + '/user_devices/:id', to: 'user_devices#destroy', via: :delete + +end diff --git a/db/migrate/20150817000001_create_user_devices.rb b/db/migrate/20150817000001_create_user_devices.rb new file mode 100644 index 000000000..013fb1d08 --- /dev/null +++ b/db/migrate/20150817000001_create_user_devices.rb @@ -0,0 +1,22 @@ + +class CreateUserDevices < ActiveRecord::Migration + def up + create_table :user_devices do |t| + t.references :user, null: false + t.string :name, limit: 250, null: false + t.string :os, limit: 150, null: true + t.string :browser, limit: 250, null: true + t.string :country, limit: 150, null: true + t.string :device_details, limit: 2500, null: true + t.string :location_details, limit: 2500, null: true + t.timestamps + end + add_index :user_devices, [:user_id] + add_index :user_devices, [:os, :browser, :country] + add_index :user_devices, [:updated_at] + end + + def down + drop_table :user_devices + end +end diff --git a/lib/models.rb b/lib/models.rb index 4158450e3..c798a9aaa 100644 --- a/lib/models.rb +++ b/lib/models.rb @@ -35,6 +35,9 @@ returns model_class = load_adapter(entry) next if !model_class next if !model_class.respond_to? :new + next if !model_class.respond_to? :table_name + table_name = model_class.table_name # handle models where not table exists, pending migrations + next if !ActiveRecord::Base.connection.tables.include?(table_name) model_object = model_class.new next if !model_object.respond_to? :attributes all[model_class] = {}