Skip to content

Commit f144434

Browse files
imorlandStyleCIBot
andauthored
feat: IP info modal and map for moderators, and integration with session info (#29)
* feat: IP info modal and map for moderators, and integration with session info * Apply fixes from StyleCI * change icon * chore: ready up for core 1.8.2 --------- Co-authored-by: StyleCI Bot <[email protected]>
1 parent af0ab7f commit f144434

File tree

12 files changed

+391
-45
lines changed

12 files changed

+391
-45
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Latest Stable Version](https://img.shields.io/packagist/v/fof/geoip.svg)](https://packagist.org/packages/fof/geoip) [![OpenCollective](https://img.shields.io/badge/opencollective-fof-blue.svg)](https://opencollective.com/fof/donate) [![Donate](https://img.shields.io/badge/donate-datitisev-important.svg)](https://datitisev.me/donate)
44

5-
A [Flarum](http://flarum.org) extension. Geolocation for your Flarum forum.
5+
A [Flarum](http://flarum.org) extension.
6+
7+
## Empower Your Flarum Moderators with GeoIP
8+
9+
Moderators play a crucial role in maintaining the health and quality of forums. With GeoIP, give them the geolocation tools they need to better understand users, make informed decisions, and maintain a safe environment. Only moderators have access to IP-based geolocation, ensuring user privacy and data security.
10+
11+
### 🌎 Key Features
12+
- **Location Insights**: Enable moderators to identify the country and region of users.
13+
- **Interactive Mapping**: Let moderators visualize user locations with an integrated map view.
14+
- **Threat Detection**: Equip moderators with the ability to highlight potentially malicious IP addresses through threat level indicators. (Via supported IP location data providers)
615

716
### Installation
817

@@ -16,6 +25,7 @@ composer require fof/geoip:"*"
1625

1726
```sh
1827
composer update fof/geoip
28+
php flarum cache:clear
1929
```
2030

2131
### Links

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
],
2121
"require": {
2222
"php": "^8.0",
23-
"flarum/core": "^1.2.0",
23+
"flarum/core": "^1.8.2",
2424
"guzzlehttp/guzzle": "^7.3"
2525
},
2626
"authors": [
@@ -43,7 +43,10 @@
4343
"name": "fas fa-map-marker-alt",
4444
"backgroundColor": "#e74c3c",
4545
"color": "#fff"
46-
}
46+
},
47+
"optional-dependencies": [
48+
"fof/ban-ips"
49+
]
4750
},
4851
"flagrow": {
4952
"discuss": "https://discuss.flarum.org/d/21493"

extend.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,7 @@
6767

6868
(new Extend\Settings())
6969
->default('fof-geoip.service', 'ipapi'),
70+
71+
(new Extend\Routes('api'))
72+
->get('/ip_info/{ip}', 'fof-geoip.api.ip_info', Api\Controller\ShowIpInfoController::class),
7073
];

js/src/forum/components/MapModal.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import app from 'flarum/forum/app';
2+
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
3+
import ZipCodeMap from './ZipCodeMap';
4+
import IPInfo from '../models/IPInfo';
5+
import { handleCopyIP } from '../helpers/ClipboardHelper';
6+
import LabelValue from 'flarum/common/components/LabelValue';
7+
import type Mithril from 'mithril';
8+
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
9+
10+
interface MapModalAttrs extends IInternalModalAttrs {
11+
ipInfo?: IPInfo;
12+
ipAddr: string;
13+
}
14+
15+
export default class MapModal extends Modal<MapModalAttrs> {
16+
ipInfo: IPInfo | undefined;
17+
18+
oninit(vnode: Mithril.Vnode<MapModalAttrs, this>) {
19+
super.oninit(vnode);
20+
this.ipInfo = this.attrs.ipInfo;
21+
if (this.ipInfo === undefined) {
22+
this.loadIpInfo();
23+
}
24+
}
25+
26+
className() {
27+
return 'MapModal Modal--medium';
28+
}
29+
30+
loadIpInfo() {
31+
app.store
32+
.find<IPInfo>('ip_info', encodeURIComponent(this.attrs.ipAddr))
33+
.then((ipInfo) => {
34+
this.ipInfo = ipInfo;
35+
m.redraw();
36+
})
37+
.catch((error) => {
38+
console.error('Failed to load IP information from the store', error);
39+
});
40+
}
41+
42+
title() {
43+
return app.translator.trans('fof-geoip.forum.map_modal.title');
44+
}
45+
46+
content() {
47+
const ipInfo = this.ipInfo;
48+
49+
if (!ipInfo) {
50+
return (
51+
<div className="Modal-body">
52+
<LoadingIndicator />
53+
</div>
54+
);
55+
}
56+
57+
return (
58+
<div className="Modal-body">
59+
<div className="IPDetails">
60+
<LabelValue
61+
label={app.translator.trans('fof-geoip.forum.map_modal.ip_address')}
62+
value={
63+
<span className="clickable-ip" onclick={handleCopyIP(this.attrs.ipAddr)}>
64+
{this.attrs.ipAddr}
65+
</span>
66+
}
67+
/>
68+
{ipInfo.countryCode() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.country_code')} value={ipInfo.countryCode()} />}
69+
{ipInfo.zipCode() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.zip_code')} value={ipInfo.zipCode()} />}
70+
{ipInfo.isp() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.isp')} value={ipInfo.isp()} />}
71+
{ipInfo.organization() && (
72+
<LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.organization')} value={ipInfo.organization()} />
73+
)}
74+
{ipInfo.threatLevel() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.threat_level')} value={ipInfo.threatLevel()} />}
75+
{ipInfo.threatTypes().length > 0 && (
76+
<LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.threat_types')} value={ipInfo.threatTypes().join(', ')} />
77+
)}
78+
{ipInfo.error() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.error')} value={ipInfo.error()} />}
79+
</div>
80+
<hr />
81+
<div id="mapContainer">
82+
<ZipCodeMap zip={ipInfo.zipCode()} country={ipInfo.countryCode()} />
83+
</div>
84+
</div>
85+
);
86+
}
87+
}

js/src/forum/components/ZipCodeMap.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ let addedResources = false;
77
const addResources = async () => {
88
if (addedResources) return;
99

10-
await load.css('https://unpkg.com/leaflet@1.5.1/dist/leaflet.css');
11-
await load.js('https://unpkg.com/leaflet@1.5.1/dist/leaflet.js');
10+
await load.css('https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
11+
await load.js('https://unpkg.com/leaflet@1.9.4/dist/leaflet.js');
1212

1313
addedResources = true;
1414
};
@@ -65,12 +65,8 @@ export default class ZipCodeMap extends Component {
6565

6666
this.map = L.map(vnode.dom).setView([51.505, -0.09], 5);
6767

68-
L.control.scale().addTo(this.map);
69-
70-
L.tileLayer(`https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png?lang=${app.data.locale}`, {
71-
attribution: '<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia</a>',
72-
minZoom: 1,
73-
maxZoom: 19,
68+
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
69+
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
7470
}).addTo(this.map);
7571

7672
L.marker([bounding[0], bounding[2]]).addTo(this.map).bindPopup(displayName).openPopup();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import app from 'flarum/forum/app';
2+
import { extend } from 'flarum/common/extend';
3+
import AccessTokensList from 'flarum/forum/components/AccessTokensList';
4+
import ItemList from 'flarum/common/utils/ItemList';
5+
import MapModal from '../components/MapModal';
6+
import Button from 'flarum/common/components/Button';
7+
import type Mithril from 'mithril';
8+
import AccessToken from 'flarum/common/models/AccessToken';
9+
10+
export default function extendAccessTokensList() {
11+
extend(AccessTokensList.prototype, 'tokenActionItems', function (items: ItemList<Mithril.Children>, token: AccessToken) {
12+
const ipAddr = token.lastIpAddress();
13+
14+
if (ipAddr) {
15+
items.add(
16+
'geoip-info',
17+
<Button
18+
className="Button"
19+
onclick={() => app.modal.show(MapModal, { ipAddr: ipAddr })}
20+
aria-label={app.translator.trans('fof-geoip.forum.map_button_label')}
21+
>
22+
{app.translator.trans('fof-geoip.forum.map_button_label')}
23+
</Button>,
24+
10
25+
);
26+
}
27+
});
28+
}

js/src/forum/extenders/extendPostMeta.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,77 @@ import PostMeta from 'flarum/common/components/PostMeta';
44
import Tooltip from 'flarum/common/components/Tooltip';
55
import { getIPData } from '../helpers/IPDataHelper';
66
import { handleCopyIP } from '../helpers/ClipboardHelper';
7+
import ItemList from 'flarum/common/utils/ItemList';
8+
import Button from 'flarum/common/components/Button';
9+
import MapModal from '../components/MapModal';
710

811
export default function extendPostMeta() {
912
extend(PostMeta.prototype, 'view', function (vdom) {
1013
const post = this.attrs.post;
14+
15+
// Exit early if there's no post
1116
if (!post) return;
1217

1318
const ipInformation = post.ip_info();
14-
const ipAddr = post.data.attributes.ipAddress;
1519

20+
// Exit early if there's no IP information for the post
1621
if (!ipInformation) return;
1722

23+
// Extract dropdown from the VDOM
1824
const menuDropdown = vdom.children.find((e) => e.attrs?.className?.includes('dropdown-menu'));
19-
const ipElement = menuDropdown.children.find((e) => e.tag === 'span' && e.attrs?.className === 'PostMeta-ip');
2025

21-
const { description, threat, image } = getIPData(ipInformation);
26+
// Extract IP element for modification
27+
const ipElement = menuDropdown.children.find((e) => e.tag === 'span' && e.attrs?.className === 'PostMeta-ip');
2228

29+
// Clear any default text from the IP element
2330
delete ipElement.text;
2431

25-
ipElement.children = [
26-
<Tooltip text={description + (!!threat ? ` (${threat})` : '')}>
27-
<span onclick={ipAddr && handleCopyIP(ipAddr)}>{ipAddr}</span>
28-
</Tooltip>,
29-
];
30-
31-
if (image) {
32-
ipElement.children.unshift(image);
33-
}
32+
// Construct the IP element with the tooltip and interactive behavior
33+
ipElement.children = [<div className="ip-container">{this.ipItems().toArray()}</div>];
3434

35+
// If there's a threat level, add it as a data attribute for potential styling
36+
// TODO: move this to an Item?
3537
if (ipInformation.threatLevel) {
3638
ipElement.attrs['data-threat-level'] = ipInformation.threatLevel();
3739
}
3840
});
41+
42+
PostMeta.prototype.ipItems = function () {
43+
const items = new ItemList();
44+
const post = this.attrs.post;
45+
const ipInformation = post.ip_info();
46+
const ipAddr = post.data.attributes.ipAddress;
47+
48+
if (ipInformation && ipAddr) {
49+
const { description, threat, image, zip, country } = getIPData(ipInformation);
50+
51+
items.add(
52+
'ipInfo',
53+
<div className="ip-info">
54+
{image}
55+
<Tooltip text={`${description} ${threat ? `(${threat})` : ''}`}>
56+
<span onclick={handleCopyIP(ipAddr)}>{ipAddr}</span>
57+
</Tooltip>
58+
</div>,
59+
100
60+
);
61+
62+
if (zip && country) {
63+
items.add(
64+
'mapButton',
65+
<Tooltip text={app.translator.trans('fof-geoip.forum.map_button_label')}>
66+
<Button
67+
icon="fas fa-map-marker-alt"
68+
className="Button Button--icon Button--link"
69+
onclick={() => app.modal.show(MapModal, { ipInfo: ipInformation, ipAddr: ipAddr })}
70+
aria-label={app.translator.trans('fof-geoip.forum.map_button_label')}
71+
/>
72+
</Tooltip>,
73+
90
74+
);
75+
}
76+
}
77+
78+
return items;
79+
};
3980
}

js/src/forum/helpers/IPDataHelper.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,10 @@ export const getIPData = (ipInfo: IPInfo) => {
2828
const description = getDescription(ipInfo);
2929
const threat = getThreat(ipInfo);
3030
const image = getFlagImage(ipInfo);
31-
return { description, threat, image };
31+
32+
// Extracting zip and country from ipInfo
33+
const zip = ipInfo.zipCode();
34+
const country = ipInfo.countryCode(); // Assuming the country code is used as 'country'
35+
36+
return { description, threat, image, zip, country };
3237
};

js/src/forum/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import app from 'flarum/forum/app';
22
import extendPostMeta from './extenders/extendPostMeta';
33
import extendBanIPModal from './extenders/extendBanIPModal';
4+
import extendAccessTokensList from './extenders/extendAccessTokensList';
45

56
export { default as extend } from './extend';
67

78
app.initializers.add('fof/geoip', () => {
89
extendPostMeta();
910
extendBanIPModal();
11+
12+
// This cannot be enabled until https://github.com/flarum/framework/pull/3888 is merged and released
13+
extendAccessTokensList();
1014
});

0 commit comments

Comments
 (0)