FEATURE: adds security tab to dashboard (#6768)

This commit also includes the new staff_logins report
This commit is contained in:
Joffrey JAFFEUX 2018-12-14 13:47:59 +01:00 committed by GitHub
parent 9f89aadd33
commit 03014b0d05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 147 additions and 17 deletions

View File

@ -276,9 +276,13 @@ const Report = Discourse.Model.extend({
return this._numberLabel(value, opts); return this._numberLabel(value, opts);
} }
if (type === "date") { if (type === "date") {
const date = moment(value, "YYYY-MM-DD"); const date = moment(value);
if (date.isValid()) return this._dateLabel(value, date); 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); if (type === "text") return this._textLabel(value);
return { return {
@ -377,10 +381,10 @@ const Report = Discourse.Model.extend({
}; };
}, },
_dateLabel(value, date) { _dateLabel(value, date, format = "LL") {
return { return {
value, value,
formatedValue: value ? date.format("LL") : "—" formatedValue: value ? date.format(format) : "—"
}; };
}, },

View File

@ -8,6 +8,10 @@ export default function() {
path: "/dashboard/moderation", path: "/dashboard/moderation",
resetNamespace: true resetNamespace: true
}); });
this.route("admin.dashboardNextSecurity", {
path: "/dashboard/security",
resetNamespace: true
});
}); });
this.route( this.route(

View File

@ -21,6 +21,11 @@
{{i18n "admin.dashboard.moderation_tab"}} {{i18n "admin.dashboard.moderation_tab"}}
{{/link-to}} {{/link-to}}
</li> </li>
<li class="navigation-item security">
{{#link-to "admin.dashboardNextSecurity" class="navigation-link"}}
{{i18n "admin.dashboard.security_tab"}}
{{/link-to}}
</li>
</ul> </ul>
{{outlet}} {{outlet}}

View File

@ -0,0 +1,15 @@
<div class="sections">
{{plugin-outlet name="admin-dashboard-security-top"}}
<div class="main-section">
{{admin-report
dataSourceName="suspicious_logins"
filters=lastWeekfilters}}
{{admin-report
dataSourceName="staff_logins"
filters=lastWeekfilters}}
{{plugin-outlet name="admin-dashboard-security-bottom"}}
</div>
</div>

View File

@ -39,6 +39,10 @@
@include active-navigation-item; @include active-navigation-item;
} }
&.dashboard-next-security .navigation-item.security {
@include active-navigation-item;
}
&.general .navigation-item.general { &.general .navigation-item.general {
@include active-navigation-item; @include active-navigation-item;
} }
@ -488,14 +492,8 @@
margin-bottom: 1.5em; margin-bottom: 1.5em;
} }
.dashboard-next-moderation { .dashboard-next-moderation,
.admin-dashboard-moderation-top { .dashboard-next-security {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 1em;
grid-row-gap: 1em;
}
.section-body { .section-body {
margin-bottom: 1em; margin-bottom: 1em;
} }
@ -510,6 +508,7 @@
grid-column: span 12; grid-column: span 12;
} }
.admin-dashboard-security-bottom-outlet,
.admin-dashboard-moderation-bottom-outlet { .admin-dashboard-moderation-bottom-outlet {
display: grid; display: grid;
grid-template-columns: repeat(12, 1fr); grid-template-columns: repeat(12, 1fr);
@ -518,11 +517,16 @@
} }
} }
.admin-report.flags-status { .admin-report {
grid-column: span 12;
}
.admin-report.post-edits {
grid-column: span 12; 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;
}
}

View File

@ -12,6 +12,7 @@ class Admin::DashboardNextController < Admin::AdminController
end end
def moderation; end def moderation; end
def security; end
def general def general
data = AdminDashboardNextGeneralData.fetch_cached_stats data = AdminDashboardNextGeneralData.fetch_cached_stats

View File

@ -1276,6 +1276,64 @@ class Report
end end
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) def self.report_suspicious_logins(report)
report.modes = [:table] report.modes = [:table]

View File

@ -2854,6 +2854,7 @@ en:
all_reports: "All reports" all_reports: "All reports"
general_tab: "General" general_tab: "General"
moderation_tab: "Moderation" moderation_tab: "Moderation"
security_tab: "Security"
disabled: Disabled disabled: Disabled
timeout_error: Sorry, query is taking too long, please pick a shorter interval timeout_error: Sorry, query is taking too long, please pick a shorter interval
exception_error: Sorry, an error occurred while executing the query exception_error: Sorry, an error occurred while executing the query

View File

@ -1134,6 +1134,12 @@ en:
device: Device device: Device
os: Operating System os: Operating System
login_time: Login Time login_time: Login Time
staff_logins:
title: "Staff logins"
labels:
user: User
location: Location
login_at: Login at
dashboard: dashboard:
rails_env_warning: "Your server is running in %{env} mode." rails_env_warning: "Your server is running in %{env} mode."

View File

@ -241,6 +241,7 @@ Discourse::Application.routes.draw do
get "dashboard" => "dashboard_next#index" get "dashboard" => "dashboard_next#index"
get "dashboard/general" => "dashboard_next#general" get "dashboard/general" => "dashboard_next#general"
get "dashboard/moderation" => "dashboard_next#moderation" get "dashboard/moderation" => "dashboard_next#moderation"
get "dashboard/security" => "dashboard_next#security"
get "dashboard-old" => "dashboard#index" get "dashboard-old" => "dashboard#index"

View File

@ -42,7 +42,7 @@ class DiscourseIpInfo
ret[:city] = result.city.name(locale) || result.city.name ret[:city] = result.city.name(locale) || result.city.name
ret[:latitude] = result.location.latitude ret[:latitude] = result.location.latitude
ret[:longitude] = result.location.longitude 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 end
rescue => e rescue => e
Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.") Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.")

View File

@ -995,6 +995,37 @@ describe Report do
expect(report.data[2][:username]).to eq("joffrey") expect(report.data[2][:username]).to eq("joffrey")
end end
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
end end