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);
|
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) : "—"
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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;
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue