diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6
index 709dac0a52b..6fc812f349f 100644
--- a/app/assets/javascripts/admin/models/report.js.es6
+++ b/app/assets/javascripts/admin/models/report.js.es6
@@ -276,9 +276,13 @@ const Report = Discourse.Model.extend({
return this._numberLabel(value, opts);
}
if (type === "date") {
- const date = moment(value, "YYYY-MM-DD");
+ const date = moment(value);
if (date.isValid()) return this._dateLabel(value, date);
}
+ if (type === "precise_date") {
+ const date = moment(value);
+ if (date.isValid()) return this._dateLabel(value, date, "LLL");
+ }
if (type === "text") return this._textLabel(value);
return {
@@ -377,10 +381,10 @@ const Report = Discourse.Model.extend({
};
},
- _dateLabel(value, date) {
+ _dateLabel(value, date, format = "LL") {
return {
value,
- formatedValue: value ? date.format("LL") : "—"
+ formatedValue: value ? date.format(format) : "—"
};
},
diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
index 89adcfbb157..4ce0b8f3137 100644
--- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
@@ -8,6 +8,10 @@ export default function() {
path: "/dashboard/moderation",
resetNamespace: true
});
+ this.route("admin.dashboardNextSecurity", {
+ path: "/dashboard/security",
+ resetNamespace: true
+ });
});
this.route(
diff --git a/app/assets/javascripts/admin/templates/dashboard_next.hbs b/app/assets/javascripts/admin/templates/dashboard_next.hbs
index 176f90c22de..e7b1e1c5767 100644
--- a/app/assets/javascripts/admin/templates/dashboard_next.hbs
+++ b/app/assets/javascripts/admin/templates/dashboard_next.hbs
@@ -21,6 +21,11 @@
{{i18n "admin.dashboard.moderation_tab"}}
{{/link-to}}
+
+ {{#link-to "admin.dashboardNextSecurity" class="navigation-link"}}
+ {{i18n "admin.dashboard.security_tab"}}
+ {{/link-to}}
+
{{outlet}}
diff --git a/app/assets/javascripts/admin/templates/dashboard_next_security.hbs b/app/assets/javascripts/admin/templates/dashboard_next_security.hbs
new file mode 100644
index 00000000000..a42e65d58ad
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/dashboard_next_security.hbs
@@ -0,0 +1,15 @@
+
+ {{plugin-outlet name="admin-dashboard-security-top"}}
+
+
+ {{admin-report
+ dataSourceName="suspicious_logins"
+ filters=lastWeekfilters}}
+
+ {{admin-report
+ dataSourceName="staff_logins"
+ filters=lastWeekfilters}}
+
+ {{plugin-outlet name="admin-dashboard-security-bottom"}}
+
+
diff --git a/app/assets/stylesheets/common/admin/dashboard_next.scss b/app/assets/stylesheets/common/admin/dashboard_next.scss
index db101eb4317..1ba8f9a6c83 100644
--- a/app/assets/stylesheets/common/admin/dashboard_next.scss
+++ b/app/assets/stylesheets/common/admin/dashboard_next.scss
@@ -39,6 +39,10 @@
@include active-navigation-item;
}
+ &.dashboard-next-security .navigation-item.security {
+ @include active-navigation-item;
+ }
+
&.general .navigation-item.general {
@include active-navigation-item;
}
@@ -488,14 +492,8 @@
margin-bottom: 1.5em;
}
-.dashboard-next-moderation {
- .admin-dashboard-moderation-top {
- display: grid;
- grid-template-columns: repeat(12, 1fr);
- grid-column-gap: 1em;
- grid-row-gap: 1em;
- }
-
+.dashboard-next-moderation,
+.dashboard-next-security {
.section-body {
margin-bottom: 1em;
}
@@ -510,6 +508,7 @@
grid-column: span 12;
}
+ .admin-dashboard-security-bottom-outlet,
.admin-dashboard-moderation-bottom-outlet {
display: grid;
grid-template-columns: repeat(12, 1fr);
@@ -518,11 +517,16 @@
}
}
- .admin-report.flags-status {
- grid-column: span 12;
- }
-
- .admin-report.post-edits {
+ .admin-report {
grid-column: span 12;
}
}
+
+.dashboard-next-moderation {
+ .admin-dashboard-moderation-top {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ grid-column-gap: 1em;
+ grid-row-gap: 1em;
+ }
+}
diff --git a/app/controllers/admin/dashboard_next_controller.rb b/app/controllers/admin/dashboard_next_controller.rb
index 8da41c075db..41360bd7e57 100644
--- a/app/controllers/admin/dashboard_next_controller.rb
+++ b/app/controllers/admin/dashboard_next_controller.rb
@@ -12,6 +12,7 @@ class Admin::DashboardNextController < Admin::AdminController
end
def moderation; end
+ def security; end
def general
data = AdminDashboardNextGeneralData.fetch_cached_stats
diff --git a/app/models/report.rb b/app/models/report.rb
index 09ad16684ec..19b0b70922e 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -1276,6 +1276,64 @@ class Report
end
end
+ def self.report_staff_logins(report)
+ report.modes = [:table]
+
+ report.data = []
+
+ report.labels = [
+ {
+ type: :user,
+ properties: {
+ username: :username,
+ id: :user_id,
+ avatar: :avatar_template,
+ },
+ title: I18n.t("reports.staff_logins.labels.user")
+ },
+ {
+ property: :location,
+ title: I18n.t("reports.staff_logins.labels.location")
+ },
+ {
+ property: :created_at,
+ type: :precise_date,
+ title: I18n.t("reports.staff_logins.labels.login_at")
+ }
+ ]
+
+ sql = <<~SQL
+ SELECT
+ t1.created_at created_at,
+ t1.client_ip client_ip,
+ u.username username,
+ u.uploaded_avatar_id uploaded_avatar_id,
+ u.id user_id
+ FROM (
+ SELECT DISTINCT ON (t.client_ip, t.user_id) t.client_ip, t.user_id, t.created_at
+ FROM user_auth_token_logs t
+ WHERE t.user_id IN (#{User.admins.pluck(:id).join(',')})
+ AND t.created_at >= :start_date
+ AND t.created_at <= :end_date
+ ORDER BY t.client_ip, t.user_id, t.created_at DESC
+ LIMIT #{report.limit || 20}
+ ) t1
+ JOIN users u ON u.id = t1.user_id
+ ORDER BY created_at DESC
+ SQL
+
+ DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row|
+ data = {}
+ data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id)
+ data[:user_id] = row.user_id
+ data[:username] = row.username
+ data[:location] = DiscourseIpInfo.get(row.client_ip)[:location]
+ data[:created_at] = row.created_at
+
+ report.data << data
+ end
+ end
+
def self.report_suspicious_logins(report)
report.modes = [:table]
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 33d9dbd4d3f..548fd101945 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -2854,6 +2854,7 @@ en:
all_reports: "All reports"
general_tab: "General"
moderation_tab: "Moderation"
+ security_tab: "Security"
disabled: Disabled
timeout_error: Sorry, query is taking too long, please pick a shorter interval
exception_error: Sorry, an error occurred while executing the query
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 4b1d90efb33..f3eacbe9b65 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1134,6 +1134,12 @@ en:
device: Device
os: Operating System
login_time: Login Time
+ staff_logins:
+ title: "Staff logins"
+ labels:
+ user: User
+ location: Location
+ login_at: Login at
dashboard:
rails_env_warning: "Your server is running in %{env} mode."
diff --git a/config/routes.rb b/config/routes.rb
index b16f3330882..c46c943ca65 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -241,6 +241,7 @@ Discourse::Application.routes.draw do
get "dashboard" => "dashboard_next#index"
get "dashboard/general" => "dashboard_next#general"
get "dashboard/moderation" => "dashboard_next#moderation"
+ get "dashboard/security" => "dashboard_next#security"
get "dashboard-old" => "dashboard#index"
diff --git a/lib/discourse_ip_info.rb b/lib/discourse_ip_info.rb
index cefeaa64b47..343312c6968 100644
--- a/lib/discourse_ip_info.rb
+++ b/lib/discourse_ip_info.rb
@@ -42,7 +42,7 @@ class DiscourseIpInfo
ret[:city] = result.city.name(locale) || result.city.name
ret[:latitude] = result.location.latitude
ret[:longitude] = result.location.longitude
- ret[:location] = [ret[:city], ret[:region], ret[:country]].reject(&:blank?).join(", ")
+ ret[:location] = ret.values_at(:city, :region, :country).reject(&:blank?).uniq.join(", ")
end
rescue => e
Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.")
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index b0a43dc712e..aa6d86ae5f5 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -995,6 +995,37 @@ describe Report do
expect(report.data[2][:username]).to eq("joffrey")
end
end
+ end
+ describe "report_staff_logins" do
+ let(:joffrey) { Fabricate(:admin, username: "joffrey") }
+ let(:robin) { Fabricate(:admin, username: "robin") }
+ let(:james) { Fabricate(:user, username: "james") }
+
+ context "with data" do
+ it "works" do
+ freeze_time DateTime.parse('2017-03-01 12:00')
+
+ ip = [81, 2, 69, 142]
+
+ DiscourseIpInfo.open_db(File.join(Rails.root, 'spec', 'fixtures', 'mmdb'))
+ Resolv::DNS.any_instance.stubs(:getname).with(ip.join(".")).returns("ip-#{ip.join("-")}.example.com")
+
+ UserAuthToken.log(action: "generate", user_id: robin.id, client_ip: ip.join("."), created_at: 1.hour.ago)
+ UserAuthToken.log(action: "generate", user_id: joffrey.id, client_ip: "1.2.3.4")
+ UserAuthToken.log(action: "generate", user_id: joffrey.id, client_ip: ip.join("."), created_at: 2.hours.ago)
+ UserAuthToken.log(action: "generate", user_id: james.id)
+
+ report = Report.find("staff_logins")
+
+ expect(report.data.length).to eq(3)
+ expect(report.data[0][:username]).to eq("joffrey")
+
+ expect(report.data[1][:username]).to eq("robin")
+ expect(report.data[1][:location]).to eq("London, England, United Kingdom")
+
+ expect(report.data[2][:username]).to eq("joffrey")
+ end
+ end
end
end