Skip to content

Commit ec52a0f

Browse files
authored
Merge pull request #8 from devforth/feature/AdminForth/292/add-ability-to-store-county-is
feat: Add displaying the country depending on the IP
2 parents 52633bc + 936e79d commit ec52a0f

File tree

2 files changed

+76
-3
lines changed

2 files changed

+76
-3
lines changed

index.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
import dayjs from 'dayjs'
88
import utc from 'dayjs/plugin/utc.js';
99

10-
import { AdminForthPlugin, AllowedActionsEnum, AdminForthSortDirections, AdminForthDataTypes, HttpExtra, ActionCheckSource, } from "adminforth";
10+
import { AdminForthPlugin, AllowedActionsEnum, AdminForthSortDirections, AdminForthDataTypes, HttpExtra, ActionCheckSource, Filters, } from "adminforth";
1111
import { PluginOptions } from "./types.js";
1212

1313
dayjs.extend(utc);
@@ -29,6 +29,64 @@ export default class AuditLogPlugin extends AdminForthPlugin {
2929

3030
static defaultError = 'Sorry, you do not have access to this resource.'
3131

32+
async getIpAndCountry(headers: Record<string, any>): Promise<{ country: string | null, clientIp: string | null }> {
33+
let clientIp: string | null = null;
34+
if (this.options.resourceColumns.resourceIpColumnName) {
35+
clientIp = this.adminforth.auth.getClientIp(headers);
36+
}
37+
let country: string | null = null;
38+
if (this.options.resourceColumns.resourceCountryColumnName && clientIp) {
39+
country = await this.getClientIpCountry(headers, clientIp);
40+
}
41+
return { country, clientIp };
42+
}
43+
44+
async getClientIpCountry(headers: Record<string, any>, clientIp: string | null): Promise<string | null> {
45+
const headersLower = Object.keys(headers).reduce((acc: Record<string, any>, key: string) => {
46+
acc[key.toLowerCase()] = headers[key];
47+
return acc;
48+
}, {});
49+
50+
if (this.options.isoCountryCodeRequestHeader) {
51+
const cfCountry = headersLower[this.options.isoCountryCodeRequestHeader.toLowerCase()];
52+
if (cfCountry && cfCountry !== 'XX') {
53+
return cfCountry.toUpperCase();
54+
}
55+
}
56+
57+
// DB CHECK
58+
const ipCol = this.options.resourceColumns.resourceIpColumnName;
59+
const countryCol = this.options.resourceColumns.resourceCountryColumnName;
60+
61+
//TODO fix ts-ignore after release new adminforth version with proper types
62+
//@ts-ignore
63+
const existingLog = await this.adminforth.resource(this.auditLogResource).get(Filters.AND(Filters.EQ(ipCol, clientIp), Filters.IS_NOT_EMPTY(countryCol)));
64+
if (existingLog) {
65+
return existingLog[countryCol];
66+
}
67+
68+
// API Request
69+
try {
70+
const apiUrl = `https://ipinfo.io/${clientIp}/json`;
71+
72+
const response = await fetch(apiUrl);
73+
if (response.status !== 200) {
74+
return null;
75+
}
76+
77+
const data: any = await response.json();
78+
const country = data.country;
79+
80+
if (country && typeof country === 'string' && country.length === 2) {
81+
return country.toUpperCase();
82+
}
83+
84+
} catch (e) {
85+
console.error('Error fetching IP country', e);
86+
}
87+
88+
return null;
89+
}
3290
createLogRecord = async (resource: AdminForthResource, action: AllowedActionsEnum | string, data: Object, user: AdminUser, oldRecord?: Object, extra?: HttpExtra) => {
3391
const recordIdFieldName = resource.columns.find((c) => c.primaryKey === true)?.name;
3492
const recordId = data?.[recordIdFieldName] || oldRecord?.[recordIdFieldName];
@@ -85,6 +143,8 @@ export default class AuditLogPlugin extends AdminForthPlugin {
85143
}
86144
});
87145

146+
const { country, clientIp } = await this.getIpAndCountry(extra?.headers || {});
147+
88148
const record = {
89149
[this.options.resourceColumns.resourceIdColumnName]: resource.resourceId,
90150
[this.options.resourceColumns.resourceActionColumnName]: action,
@@ -93,7 +153,8 @@ export default class AuditLogPlugin extends AdminForthPlugin {
93153
[this.options.resourceColumns.resourceRecordIdColumnName]: recordId,
94154
// utc iso string
95155
[this.options.resourceColumns.resourceCreatedColumnName]: dayjs.utc().format(),
96-
...(this.options.resourceColumns.resourceIpColumnName && extra?.headers ? {[this.options.resourceColumns.resourceIpColumnName]: this.adminforth.auth.getClientIp(extra.headers)} : {}),
156+
...(clientIp ? {[this.options.resourceColumns.resourceIpColumnName]: clientIp} : {}),
157+
...(country ? {[this.options.resourceColumns.resourceCountryColumnName]: country } : {}),
97158
}
98159
const auditLogResource = this.adminforth.config.resources.find((r) => r.resourceId === this.auditLogResource);
99160
await this.adminforth.createResourceRecord({ resource: auditLogResource, record, adminUser: user});
@@ -132,15 +193,19 @@ export default class AuditLogPlugin extends AdminForthPlugin {
132193
}
133194
}
134195

196+
const { country, clientIp } = await this.getIpAndCountry(headers || {});
197+
135198
const record = {
136199
[this.options.resourceColumns.resourceIdColumnName]: resourceId,
137200
[this.options.resourceColumns.resourceActionColumnName]: actionId,
138201
[this.options.resourceColumns.resourceDataColumnName]: { 'oldRecord': oldData || {}, 'newRecord': data },
139202
[this.options.resourceColumns.resourceUserIdColumnName]: user.pk,
140203
[this.options.resourceColumns.resourceRecordIdColumnName]: recordId,
141204
[this.options.resourceColumns.resourceCreatedColumnName]: dayjs.utc().format(),
142-
...(this.options.resourceColumns.resourceIpColumnName && headers ? {[this.options.resourceColumns.resourceIpColumnName]: this.adminforth.auth.getClientIp(headers)} : {}),
205+
...(clientIp ? {[this.options.resourceColumns.resourceIpColumnName]: clientIp} : {}),
206+
...(country ? {[this.options.resourceColumns.resourceCountryColumnName]: country } : {}),
143207
}
208+
144209
const auditLogResource = this.adminforth.config.resources.find((r) => r.resourceId === this.auditLogResource);
145210
await this.adminforth.createResourceRecord({ resource: auditLogResource, record, adminUser: user});
146211
}

types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,16 @@ export type PluginOptions = {
3535
resourceRecordIdColumnName: string
3636

3737
resourceCreatedColumnName: string
38+
39+
resourceCountryColumnName?: string
3840

3941
resourceIpColumnName?: string
4042
}
43+
4144

45+
/*
46+
* should be in format ISO 3166-1 alpha-2
47+
* e.g. for ckloudflare it should be 'CF-IPCountry'
48+
*/
49+
isoCountryCodeRequestHeader?: string;
4250
}

0 commit comments

Comments
 (0)