Skip to content

Commit

Permalink
feat: IP info modal and map for moderators, and integration with sess…
Browse files Browse the repository at this point in the history
…ion 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]>
  • Loading branch information
imorland and StyleCIBot authored Sep 22, 2023
1 parent af0ab7f commit f144434
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 45 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

![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)

A [Flarum](http://flarum.org) extension. Geolocation for your Flarum forum.
A [Flarum](http://flarum.org) extension.

## Empower Your Flarum Moderators with GeoIP

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.

### 🌎 Key Features
- **Location Insights**: Enable moderators to identify the country and region of users.
- **Interactive Mapping**: Let moderators visualize user locations with an integrated map view.
- **Threat Detection**: Equip moderators with the ability to highlight potentially malicious IP addresses through threat level indicators. (Via supported IP location data providers)

### Installation

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

```sh
composer update fof/geoip
php flarum cache:clear
```

### Links
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
],
"require": {
"php": "^8.0",
"flarum/core": "^1.2.0",
"flarum/core": "^1.8.2",
"guzzlehttp/guzzle": "^7.3"
},
"authors": [
Expand All @@ -43,7 +43,10 @@
"name": "fas fa-map-marker-alt",
"backgroundColor": "#e74c3c",
"color": "#fff"
}
},
"optional-dependencies": [
"fof/ban-ips"
]
},
"flagrow": {
"discuss": "https://discuss.flarum.org/d/21493"
Expand Down
3 changes: 3 additions & 0 deletions extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,7 @@

(new Extend\Settings())
->default('fof-geoip.service', 'ipapi'),

(new Extend\Routes('api'))
->get('/ip_info/{ip}', 'fof-geoip.api.ip_info', Api\Controller\ShowIpInfoController::class),
];
87 changes: 87 additions & 0 deletions js/src/forum/components/MapModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import app from 'flarum/forum/app';
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import ZipCodeMap from './ZipCodeMap';
import IPInfo from '../models/IPInfo';
import { handleCopyIP } from '../helpers/ClipboardHelper';
import LabelValue from 'flarum/common/components/LabelValue';
import type Mithril from 'mithril';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';

interface MapModalAttrs extends IInternalModalAttrs {
ipInfo?: IPInfo;
ipAddr: string;
}

export default class MapModal extends Modal<MapModalAttrs> {
ipInfo: IPInfo | undefined;

oninit(vnode: Mithril.Vnode<MapModalAttrs, this>) {
super.oninit(vnode);
this.ipInfo = this.attrs.ipInfo;
if (this.ipInfo === undefined) {
this.loadIpInfo();
}
}

className() {
return 'MapModal Modal--medium';
}

loadIpInfo() {
app.store
.find<IPInfo>('ip_info', encodeURIComponent(this.attrs.ipAddr))
.then((ipInfo) => {
this.ipInfo = ipInfo;
m.redraw();
})
.catch((error) => {
console.error('Failed to load IP information from the store', error);
});
}

title() {
return app.translator.trans('fof-geoip.forum.map_modal.title');
}

content() {
const ipInfo = this.ipInfo;

if (!ipInfo) {
return (
<div className="Modal-body">
<LoadingIndicator />
</div>
);
}

return (
<div className="Modal-body">
<div className="IPDetails">
<LabelValue
label={app.translator.trans('fof-geoip.forum.map_modal.ip_address')}
value={
<span className="clickable-ip" onclick={handleCopyIP(this.attrs.ipAddr)}>
{this.attrs.ipAddr}
</span>
}
/>
{ipInfo.countryCode() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.country_code')} value={ipInfo.countryCode()} />}
{ipInfo.zipCode() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.zip_code')} value={ipInfo.zipCode()} />}
{ipInfo.isp() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.isp')} value={ipInfo.isp()} />}
{ipInfo.organization() && (
<LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.organization')} value={ipInfo.organization()} />
)}
{ipInfo.threatLevel() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.threat_level')} value={ipInfo.threatLevel()} />}
{ipInfo.threatTypes().length > 0 && (
<LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.threat_types')} value={ipInfo.threatTypes().join(', ')} />
)}
{ipInfo.error() && <LabelValue label={app.translator.trans('fof-geoip.forum.map_modal.error')} value={ipInfo.error()} />}
</div>
<hr />
<div id="mapContainer">
<ZipCodeMap zip={ipInfo.zipCode()} country={ipInfo.countryCode()} />
</div>
</div>
);
}
}
12 changes: 4 additions & 8 deletions js/src/forum/components/ZipCodeMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ let addedResources = false;
const addResources = async () => {
if (addedResources) return;

await load.css('https://unpkg.com/leaflet@1.5.1/dist/leaflet.css');
await load.js('https://unpkg.com/leaflet@1.5.1/dist/leaflet.js');
await load.css('https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
await load.js('https://unpkg.com/leaflet@1.9.4/dist/leaflet.js');

addedResources = true;
};
Expand Down Expand Up @@ -65,12 +65,8 @@ export default class ZipCodeMap extends Component {

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

L.control.scale().addTo(this.map);

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

L.marker([bounding[0], bounding[2]]).addTo(this.map).bindPopup(displayName).openPopup();
Expand Down
28 changes: 28 additions & 0 deletions js/src/forum/extenders/extendAccessTokensList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import app from 'flarum/forum/app';
import { extend } from 'flarum/common/extend';
import AccessTokensList from 'flarum/forum/components/AccessTokensList';
import ItemList from 'flarum/common/utils/ItemList';
import MapModal from '../components/MapModal';
import Button from 'flarum/common/components/Button';
import type Mithril from 'mithril';
import AccessToken from 'flarum/common/models/AccessToken';

export default function extendAccessTokensList() {
extend(AccessTokensList.prototype, 'tokenActionItems', function (items: ItemList<Mithril.Children>, token: AccessToken) {
const ipAddr = token.lastIpAddress();

if (ipAddr) {
items.add(
'geoip-info',
<Button
className="Button"
onclick={() => app.modal.show(MapModal, { ipAddr: ipAddr })}
aria-label={app.translator.trans('fof-geoip.forum.map_button_label')}
>
{app.translator.trans('fof-geoip.forum.map_button_label')}
</Button>,
10
);
}
});
}
65 changes: 53 additions & 12 deletions js/src/forum/extenders/extendPostMeta.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,77 @@ import PostMeta from 'flarum/common/components/PostMeta';
import Tooltip from 'flarum/common/components/Tooltip';
import { getIPData } from '../helpers/IPDataHelper';
import { handleCopyIP } from '../helpers/ClipboardHelper';
import ItemList from 'flarum/common/utils/ItemList';
import Button from 'flarum/common/components/Button';
import MapModal from '../components/MapModal';

export default function extendPostMeta() {
extend(PostMeta.prototype, 'view', function (vdom) {
const post = this.attrs.post;

// Exit early if there's no post
if (!post) return;

const ipInformation = post.ip_info();
const ipAddr = post.data.attributes.ipAddress;

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

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

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

// Clear any default text from the IP element
delete ipElement.text;

ipElement.children = [
<Tooltip text={description + (!!threat ? ` (${threat})` : '')}>
<span onclick={ipAddr && handleCopyIP(ipAddr)}>{ipAddr}</span>
</Tooltip>,
];

if (image) {
ipElement.children.unshift(image);
}
// Construct the IP element with the tooltip and interactive behavior
ipElement.children = [<div className="ip-container">{this.ipItems().toArray()}</div>];

// If there's a threat level, add it as a data attribute for potential styling
// TODO: move this to an Item?
if (ipInformation.threatLevel) {
ipElement.attrs['data-threat-level'] = ipInformation.threatLevel();
}
});

PostMeta.prototype.ipItems = function () {
const items = new ItemList();
const post = this.attrs.post;
const ipInformation = post.ip_info();
const ipAddr = post.data.attributes.ipAddress;

if (ipInformation && ipAddr) {
const { description, threat, image, zip, country } = getIPData(ipInformation);

items.add(
'ipInfo',
<div className="ip-info">
{image}
<Tooltip text={`${description} ${threat ? `(${threat})` : ''}`}>
<span onclick={handleCopyIP(ipAddr)}>{ipAddr}</span>
</Tooltip>
</div>,
100
);

if (zip && country) {
items.add(
'mapButton',
<Tooltip text={app.translator.trans('fof-geoip.forum.map_button_label')}>
<Button
icon="fas fa-map-marker-alt"
className="Button Button--icon Button--link"
onclick={() => app.modal.show(MapModal, { ipInfo: ipInformation, ipAddr: ipAddr })}
aria-label={app.translator.trans('fof-geoip.forum.map_button_label')}
/>
</Tooltip>,
90
);
}
}

return items;
};
}
7 changes: 6 additions & 1 deletion js/src/forum/helpers/IPDataHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ export const getIPData = (ipInfo: IPInfo) => {
const description = getDescription(ipInfo);
const threat = getThreat(ipInfo);
const image = getFlagImage(ipInfo);
return { description, threat, image };

// Extracting zip and country from ipInfo
const zip = ipInfo.zipCode();
const country = ipInfo.countryCode(); // Assuming the country code is used as 'country'

return { description, threat, image, zip, country };
};
4 changes: 4 additions & 0 deletions js/src/forum/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import app from 'flarum/forum/app';
import extendPostMeta from './extenders/extendPostMeta';
import extendBanIPModal from './extenders/extendBanIPModal';
import extendAccessTokensList from './extenders/extendAccessTokensList';

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

app.initializers.add('fof/geoip', () => {
extendPostMeta();
extendBanIPModal();

// This cannot be enabled until https://github.com/flarum/framework/pull/3888 is merged and released
extendAccessTokensList();
});
Loading

0 comments on commit f144434

Please sign in to comment.