5htp-core 0.2.6-4 → 0.2.6-5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/server/services/router/response/index.ts +3 -1
- package/src/server/services/router/response/page/document.tsx +7 -19
- package/src/server/services/router/service.ts +1 -1
- package/src/client/services/metrics/index.ts +0 -37
- package/src/server/services/metrics/detect.ts +0 -109
- package/src/server/services/metrics/index.ts +0 -272
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5htp-core",
|
|
3
3
|
"description": "Convenient TypeScript framework designed for Performance and Productivity.",
|
|
4
|
-
"version": "0.2.6-
|
|
4
|
+
"version": "0.2.6-5",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -133,7 +133,9 @@ export default class ServerResponse<
|
|
|
133
133
|
for (const serviceName in this.router.services) {
|
|
134
134
|
|
|
135
135
|
const routerService = this.router.services[serviceName];
|
|
136
|
-
|
|
136
|
+
const requestService = routerService.requestService( this.request );
|
|
137
|
+
if (requestService !== null)
|
|
138
|
+
contextServices[ serviceName ] = requestService;
|
|
137
139
|
|
|
138
140
|
}
|
|
139
141
|
|
|
@@ -19,10 +19,10 @@ import type Page from '.';
|
|
|
19
19
|
/*----------------------------------
|
|
20
20
|
- SERVICE
|
|
21
21
|
----------------------------------*/
|
|
22
|
-
export default class DocumentRenderer {
|
|
22
|
+
export default class DocumentRenderer<TRouter extends Router> {
|
|
23
23
|
|
|
24
24
|
public constructor(
|
|
25
|
-
public router:
|
|
25
|
+
public router: TRouter,
|
|
26
26
|
public app = router.app
|
|
27
27
|
) {
|
|
28
28
|
|
|
@@ -55,7 +55,7 @@ export default class DocumentRenderer {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
public async page( html: string, page: Page, response: ServerResponse<
|
|
58
|
+
public async page( html: string, page: Page, response: ServerResponse<TRouter> ) {
|
|
59
59
|
|
|
60
60
|
const fullUrl = this.router.http.publicUrl + response.request.path;
|
|
61
61
|
|
|
@@ -133,7 +133,7 @@ export default class DocumentRenderer {
|
|
|
133
133
|
</>
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
private styles( page ) {
|
|
136
|
+
private styles( page: Page ) {
|
|
137
137
|
return <>
|
|
138
138
|
<link rel="stylesheet" type="text/css" href="/public/icons.css" />
|
|
139
139
|
<link rel="preload" href="/public/client.css" as="style" />
|
|
@@ -151,7 +151,7 @@ export default class DocumentRenderer {
|
|
|
151
151
|
</>
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
private async scripts( response
|
|
154
|
+
private async scripts( response: ServerResponse<TRouter>, page: Page ) {
|
|
155
155
|
|
|
156
156
|
const context = safeStringify( response.forSsr(page) );
|
|
157
157
|
|
|
@@ -174,21 +174,9 @@ export default class DocumentRenderer {
|
|
|
174
174
|
<link rel="preload" href={script.url} as="script" />
|
|
175
175
|
<script type="text/javascript" src={script.url} {...script.attrs || {}} />
|
|
176
176
|
</> : <>
|
|
177
|
-
<script type="text/javascript" {...script.attrs || {}} id={script.id}
|
|
177
|
+
<script type="text/javascript" {...script.attrs || {}} id={script.id}
|
|
178
|
+
dangerouslySetInnerHTML={{ __html: script.inline }} />
|
|
178
179
|
</>)}
|
|
179
|
-
|
|
180
|
-
{/* Initialize GTM & GA for pagechange events */}
|
|
181
|
-
{/* TODO: append via the metrics module */}
|
|
182
|
-
{/*<script async src={"https://www.googletagmanager.com/gtag/js?id=" + this.app.config.tracking.ga.pub}></script>
|
|
183
|
-
<script dangerouslySetInnerHTML={{ __html: `
|
|
184
|
-
window.dataLayer = window.dataLayer || [];
|
|
185
|
-
function gtag(){dataLayer.push(arguments);}
|
|
186
|
-
gtag('js', new Date());
|
|
187
|
-
|
|
188
|
-
gtag('config', '${this.app.config.tracking.ga.pub}', {
|
|
189
|
-
send_page_view: false
|
|
190
|
-
});
|
|
191
|
-
`}} />*/}
|
|
192
180
|
</>
|
|
193
181
|
}
|
|
194
182
|
}
|
|
@@ -43,6 +43,6 @@ export default abstract class RouterService<TRouter extends Router = Router> {
|
|
|
43
43
|
|
|
44
44
|
public abstract register(): Promise<void>;
|
|
45
45
|
|
|
46
|
-
public abstract requestService( request: ServerRequest<TRouter> ): RequestService;
|
|
46
|
+
public abstract requestService( request: ServerRequest<TRouter> ): RequestService | null;
|
|
47
47
|
|
|
48
48
|
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// Npm
|
|
6
|
-
|
|
7
|
-
// Core
|
|
8
|
-
import type ClientApplication from '@client/app';
|
|
9
|
-
|
|
10
|
-
/*----------------------------------
|
|
11
|
-
- TYPES
|
|
12
|
-
----------------------------------*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/*----------------------------------
|
|
17
|
-
- SERVICE
|
|
18
|
-
----------------------------------*/
|
|
19
|
-
export default class ClientMetrics {
|
|
20
|
-
|
|
21
|
-
public constructor( public app: ClientApplication ) {
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public start() {
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Tracking
|
|
30
|
-
public event( name: string, params?: object ) {
|
|
31
|
-
if (!window.gtag) return;
|
|
32
|
-
if (name === 'pageview')
|
|
33
|
-
window.gtag('send', name);
|
|
34
|
-
else
|
|
35
|
-
window.gtag('event', name, params);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// Npm
|
|
6
|
-
import got from 'got';
|
|
7
|
-
|
|
8
|
-
// Core
|
|
9
|
-
import { Forbidden } from '@common/errors';
|
|
10
|
-
import ServerRequest from '../router/request';
|
|
11
|
-
|
|
12
|
-
// App
|
|
13
|
-
import app from '@server/app';
|
|
14
|
-
|
|
15
|
-
/*----------------------------------
|
|
16
|
-
- TYPES
|
|
17
|
-
----------------------------------*/
|
|
18
|
-
|
|
19
|
-
/*----------------------------------
|
|
20
|
-
- MODULE
|
|
21
|
-
----------------------------------*/
|
|
22
|
-
export default class ProtectService {
|
|
23
|
-
|
|
24
|
-
private tracking: TrackerService;
|
|
25
|
-
|
|
26
|
-
public constructor( private request: ServerRequest ) {
|
|
27
|
-
|
|
28
|
-
this.tracking = request.tracking;
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public async bots(): Promise<IP> {
|
|
33
|
-
|
|
34
|
-
// Récupération et varifications informations ip
|
|
35
|
-
const ip = await this.tracking.checkIP();
|
|
36
|
-
|
|
37
|
-
// Captcha
|
|
38
|
-
/*if (this.request.method === 'POST')
|
|
39
|
-
await this.captcha(this.request.data.captcha);*/
|
|
40
|
-
|
|
41
|
-
return ip;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public async multiaccount(ip: IP, whitelist?: string) {
|
|
45
|
-
|
|
46
|
-
// DON'T USE IT
|
|
47
|
-
// People can't try the app when another user recommands it IRL
|
|
48
|
-
// TODO: find another way to detect / avoid multi account
|
|
49
|
-
// Ex: Force login with Google only
|
|
50
|
-
|
|
51
|
-
// If IP Address already used by another account
|
|
52
|
-
/*const username = whitelist || this.request.user?.name;
|
|
53
|
-
if (ip.user_name !== undefined && ip.user_name !== null && (username === undefined || username !== ip.user_name))
|
|
54
|
-
throw new Forbidden(`
|
|
55
|
-
I noticed you're trying to use multiple accounts.
|
|
56
|
-
Only one account is allowed per IP address.
|
|
57
|
-
If you think I'm wrong, please contact me at contact@gaetan-legac.fr and I will solve the problem.
|
|
58
|
-
`);*/
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public async botsAndMultiaccount( usernameWhitelist?: string ) {
|
|
62
|
-
const ip = await this.bots();
|
|
63
|
-
await this.multiaccount(ip, usernameWhitelist);
|
|
64
|
-
return ip;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
public async conflictOfInterest( username: string, request: ServerRequest ) {
|
|
68
|
-
|
|
69
|
-
// NOTE: Don't base conflict of interest detection on IP
|
|
70
|
-
|
|
71
|
-
// If the current ip have never been used by username
|
|
72
|
-
|
|
73
|
-
/*const conflict = await sql`
|
|
74
|
-
FROM UserLogin
|
|
75
|
-
WHERE ip = ${request.ip} AND user = ${username}
|
|
76
|
-
`.exists();
|
|
77
|
-
|
|
78
|
-
return conflict;*/
|
|
79
|
-
|
|
80
|
-
return false;
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
public async captcha(token?: string) {
|
|
85
|
-
|
|
86
|
-
console.info(`Validation du captcha`, {
|
|
87
|
-
secret: app.config.http.security.recaptcha.prv,
|
|
88
|
-
response: token
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
if (!token)
|
|
92
|
-
throw new Forbidden("Le captcha n'a pas été complété.");
|
|
93
|
-
|
|
94
|
-
const res = await got.post('https://www.google.com/recaptcha/api/siteverify', {
|
|
95
|
-
body: JSON.stringify({
|
|
96
|
-
secret: app.config.http.security.recaptcha.prv,
|
|
97
|
-
response: token,
|
|
98
|
-
remoteip: null
|
|
99
|
-
})
|
|
100
|
-
}).json()
|
|
101
|
-
|
|
102
|
-
console.info(`Réponse captcha`, res);
|
|
103
|
-
|
|
104
|
-
const ok = res.success || false;
|
|
105
|
-
|
|
106
|
-
if (!ok) throw new Forbidden("Le captcha est incorrect");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
}
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// Npm
|
|
6
|
-
import locale from 'locale'
|
|
7
|
-
import dayjs from 'dayjs';
|
|
8
|
-
import got from 'got';
|
|
9
|
-
|
|
10
|
-
// Core
|
|
11
|
-
import Application, { Service } from '@server/app';
|
|
12
|
-
import type Router from '@server/services/router'
|
|
13
|
-
import type ServerRequest from '@server/services/router/request'
|
|
14
|
-
import type SQL from '@server/services/database';
|
|
15
|
-
import { arrayToObj } from '@common/data/tableaux';
|
|
16
|
-
import { Forbidden } from '@common/errors';
|
|
17
|
-
|
|
18
|
-
/*----------------------------------
|
|
19
|
-
- TYPES
|
|
20
|
-
----------------------------------*/
|
|
21
|
-
|
|
22
|
-
type IP = {
|
|
23
|
-
// Identity
|
|
24
|
-
ip: string,
|
|
25
|
-
address: string,
|
|
26
|
-
isp: string,
|
|
27
|
-
country: string,
|
|
28
|
-
iphub: number,
|
|
29
|
-
// Status
|
|
30
|
-
banned: Date,
|
|
31
|
-
banReason?: string,
|
|
32
|
-
updated?: Date
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type TrackingInfos = {
|
|
36
|
-
user: User | null,
|
|
37
|
-
ip: IP,
|
|
38
|
-
country: string,
|
|
39
|
-
langue: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type TAssoChaines = { [id: string]: string }
|
|
43
|
-
|
|
44
|
-
/*----------------------------------
|
|
45
|
-
- SERVICE
|
|
46
|
-
----------------------------------*/
|
|
47
|
-
|
|
48
|
-
export type Config = {
|
|
49
|
-
ga: {
|
|
50
|
-
pub: string,
|
|
51
|
-
prv: string,
|
|
52
|
-
secret: string,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type Hooks = {
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export default class TrackerService extends Service<Config, Hooks, Application> {
|
|
61
|
-
|
|
62
|
-
public countries!: TAssoChaines;
|
|
63
|
-
public langues!: TAssoChaines;
|
|
64
|
-
public locales!: locale.Locales;
|
|
65
|
-
|
|
66
|
-
public async register() {
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
public async start() {
|
|
71
|
-
|
|
72
|
-
await this.indexData();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private async indexData() {
|
|
79
|
-
|
|
80
|
-
// On n'oublie pas le tri alphabétique pour les listings
|
|
81
|
-
/*const [countries, langues] = await this.app.sql`
|
|
82
|
-
SELECT id, name FROM Countries ORDER BY name ASC;
|
|
83
|
-
SELECT id, name FROM Locales ORDER BY name ASC;
|
|
84
|
-
`().then(([listePays, listeLangues]) => [
|
|
85
|
-
arrayToObj(listePays, { index: 'id', val: 'name' }),
|
|
86
|
-
arrayToObj(listeLangues, { index: 'id', val: 'name' }),
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
this.countries = countries as TAssoChaines;
|
|
90
|
-
this.langues = langues as TAssoChaines;
|
|
91
|
-
this.locales = new locale.Locales(Object.keys(this.langues));*/
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
public async request( request: ServerRequest ) {
|
|
95
|
-
return new TrackingRequestService(request, this);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/*----------------------------------
|
|
100
|
-
- REQUEST SERVICE
|
|
101
|
-
----------------------------------*/
|
|
102
|
-
export class TrackingRequestService {
|
|
103
|
-
|
|
104
|
-
// Services
|
|
105
|
-
protected sql: SQL;
|
|
106
|
-
|
|
107
|
-
// Caches
|
|
108
|
-
private langue?: string;
|
|
109
|
-
private ip?: IP;
|
|
110
|
-
|
|
111
|
-
public constructor(
|
|
112
|
-
private request: ServerRequest,
|
|
113
|
-
private tracker: TrackerService,
|
|
114
|
-
) {
|
|
115
|
-
|
|
116
|
-
this.sql = tracker.sql;
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
public event( event: 'pageview' ) {
|
|
121
|
-
|
|
122
|
-
// Ne compte pas les events lorsque mode dev ou admin
|
|
123
|
-
if (this.app.env.profile == 'dev' ||
|
|
124
|
-
(this.request.user && this.request.user.roles.includes('ADMIN'))
|
|
125
|
-
)
|
|
126
|
-
return;
|
|
127
|
-
|
|
128
|
-
console.log(`[router][request] Send GA event ${event}`);
|
|
129
|
-
/*got.post(`https://www.google-analytics.com/mp/collect`
|
|
130
|
-
+ `?measurement_id=${app.config.tracking.ga.pub}`
|
|
131
|
-
+ `&api_secret=${app.config.tracking.ga.secret}`, {
|
|
132
|
-
|
|
133
|
-
body: JSON.stringify({
|
|
134
|
-
client_id: app.config.tracking.ga.prv,
|
|
135
|
-
events: [{
|
|
136
|
-
name: 'pageview',
|
|
137
|
-
params: {},
|
|
138
|
-
}]
|
|
139
|
-
})
|
|
140
|
-
})*/
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
public async infos(): Promise<TrackingInfos> {
|
|
145
|
-
|
|
146
|
-
if (this.langue === undefined)
|
|
147
|
-
this.langue = this.getLangue();
|
|
148
|
-
|
|
149
|
-
if (this.ip === undefined)
|
|
150
|
-
this.ip = await this.checkIP();
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
user: this.request.user || null,
|
|
154
|
-
langue: this.langue,
|
|
155
|
-
country: this.ip.country,
|
|
156
|
-
ip: this.ip
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
public static values({ user, ip, langue, country }: TrackingInfos) {
|
|
161
|
-
return {
|
|
162
|
-
user: user ? user.email : null,
|
|
163
|
-
ip: ip.address,
|
|
164
|
-
country,
|
|
165
|
-
langue
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private getLangue() {
|
|
170
|
-
|
|
171
|
-
const locales = new locale.Locales( this.request.headers["accept-language"] )
|
|
172
|
-
const langue = locales.best( this.tracker.locales ).language.toUpperCase();
|
|
173
|
-
|
|
174
|
-
return (langue in this.tracker.langues) ? langue : 'EN';
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
public async checkIP(): Promise<IP> {
|
|
179
|
-
|
|
180
|
-
let address: string = this.request.ip;
|
|
181
|
-
|
|
182
|
-
// Détection multicompte: 1 Compte max / IP
|
|
183
|
-
console.log('Checking IP ...', address);
|
|
184
|
-
const now: Date = new Date;
|
|
185
|
-
|
|
186
|
-
let ip = await this.sql`SELECT * FROM logs.IP WHERE address = ${address}`.first();
|
|
187
|
-
if (!ip) {
|
|
188
|
-
|
|
189
|
-
console.log(`New IP`);
|
|
190
|
-
|
|
191
|
-
ip = {
|
|
192
|
-
address,
|
|
193
|
-
meet: now,
|
|
194
|
-
activity: now,
|
|
195
|
-
user_name: this.request.user?.name,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
await this.retrieveScore(ip);
|
|
199
|
-
|
|
200
|
-
this.sql.insert("logs.IP", ip);
|
|
201
|
-
|
|
202
|
-
// Nouvelle IP
|
|
203
|
-
} else {
|
|
204
|
-
|
|
205
|
-
console.log(`Existing IP`, address, ip.banned);
|
|
206
|
-
|
|
207
|
-
// Déjà banni
|
|
208
|
-
if (ip.banned)
|
|
209
|
-
throw new Forbidden(`Banned for the following reason: ` + ip.banReason);
|
|
210
|
-
|
|
211
|
-
// Données expirées
|
|
212
|
-
const tempsDepuisDerniereMaj = dayjs().diff(ip.dateMaj, 'day');
|
|
213
|
-
if (tempsDepuisDerniereMaj >= 7) {
|
|
214
|
-
console.log(`Dernière màj il y a ` + tempsDepuisDerniereMaj + ' jours');
|
|
215
|
-
await this.retrieveScore(ip);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
ip.activity = now;
|
|
219
|
-
|
|
220
|
-
this.sql.update("logs.IP", ip, { address });
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return ip;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private async retrieveScore(ip: IP) {
|
|
228
|
-
|
|
229
|
-
console.log(ip.address, `Computing score ...`);
|
|
230
|
-
|
|
231
|
-
// Retourner true pour autoriser
|
|
232
|
-
const [iphubOk] = await Promise.all([
|
|
233
|
-
|
|
234
|
-
// IPhub = le plus fiable en premier
|
|
235
|
-
got.get('http://v2.api.iphub.info/ip/' + ip.address, {
|
|
236
|
-
headers: {
|
|
237
|
-
'X-Key': app.config.http.security.iphub
|
|
238
|
-
}
|
|
239
|
-
}).json().then((iphub: any) => {
|
|
240
|
-
|
|
241
|
-
ip.iphub = iphub.block;
|
|
242
|
-
ip.country = iphub.countryCode;
|
|
243
|
-
ip.isp = iphub.isp;
|
|
244
|
-
|
|
245
|
-
console.log(ip.address, "IpHub:", iphub);
|
|
246
|
-
|
|
247
|
-
/*
|
|
248
|
-
block: 0 - Residential or business IP (i.e. safe IP)
|
|
249
|
-
block: 1 - Non-residential IP (hosting provider, proxy, etc.)
|
|
250
|
-
block: 2 - Non-residential & residential IP (warning, may flag innocent people)
|
|
251
|
-
*/
|
|
252
|
-
return ip.iphub !== 1;
|
|
253
|
-
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
]);
|
|
257
|
-
|
|
258
|
-
// Impossible d'avoir le country = douteux
|
|
259
|
-
// NOTE: Pour Iphub, ZZ = inconnu
|
|
260
|
-
if (!(ip.country in this.tracker.countries)) {
|
|
261
|
-
ip.banned = new Date;
|
|
262
|
-
ip.banReason = "Invalid location: " + ip.country;
|
|
263
|
-
// Considéré comme suspect par iphub etou getipintel
|
|
264
|
-
} else if (!(iphubOk)) {
|
|
265
|
-
ip.banned = new Date;
|
|
266
|
-
ip.banReason = "Suspicious activity on your network";
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
ip.updated = new Date;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
}
|