FEATURE: adds security tab to dashboard (#6768)
This commit also includes the new staff_logins report
This commit is contained in:
parent
9f89aadd33
commit
03014b0d05
|
@ -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) : "—"
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ export default function() {
|
|||
path: "/dashboard/moderation",
|
||||
resetNamespace: true
|
||||
});
|
||||
this.route("admin.dashboardNextSecurity", {
|
||||
path: "/dashboard/security",
|
||||
resetNamespace: true
|
||||
});
|
||||
});
|
||||
|
||||
this.route(
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
{{i18n "admin.dashboard.moderation_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="navigation-item security">
|
||||
{{#link-to "admin.dashboardNextSecurity" class="navigation-link"}}
|
||||
{{i18n "admin.dashboard.security_tab"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{outlet}}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ class Admin::DashboardNextController < Admin::AdminController
|
|||
end
|
||||
|
||||
def moderation; end
|
||||
def security; end
|
||||
|
||||
def general
|
||||
data = AdminDashboardNextGeneralData.fetch_cached_stats
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue