Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: IP info modal and map for moderators, and integration with session info #29

Merged
merged 4 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading