5htp-core 0.0.6 → 0.0.8-1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "5-HTP, scientifically called 5-Hydroxytryptophan, is the precursor of happiness neurotransmitter.",
4
- "version": "0.0.6",
4
+ "version": "0.0.8-1",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -37,6 +37,7 @@
37
37
  "fast-safe-stringify": "^2.1.1",
38
38
  "fs-extra": "^10.1.0",
39
39
  "google-auth-library": "^7.11.0",
40
+ "got": "^11.8.3",
40
41
  "handlebars": "^4.7.7",
41
42
  "helmet": "^4.6.0",
42
43
  "history": "^5.0.1",
@@ -57,7 +58,6 @@
57
58
  "nodemailer": "^6.6.3",
58
59
  "path-to-regexp": "^6.2.0",
59
60
  "picomatch": "^2.3.1",
60
- "preact-render-to-string": "^5.1.19",
61
61
  "react-scrollbars-custom": "^4.0.27",
62
62
  "react-slider": "^2.0.1",
63
63
  "react-textarea-autosize": "^8.3.3",
@@ -72,8 +72,10 @@
72
72
  "uuid-by-string": "^3.0.4",
73
73
  "validator": "^13.7.0",
74
74
  "ws": "^8.2.2",
75
- "yaml": "^1.10.2"
76
- },
75
+ "yaml": "^1.10.2",
76
+ "preact": "^10.5.15",
77
+ "preact-render-to-string": "^5.1.19"
78
+ },
77
79
  "devDependencies": {
78
80
  "@types/cookie": "^0.4.1",
79
81
  "@types/express": "^4.17.13",
@@ -18,7 +18,7 @@
18
18
  font-weight: 600;
19
19
 
20
20
  // Colors
21
- background: #fff;
21
+ background: transparent;
22
22
  color: var(--cTxtBase);
23
23
 
24
24
  &,
@@ -202,7 +202,7 @@ export class ClientContext {
202
202
  public handleError(e: Error) {
203
203
  switch (e.http) {
204
204
  case 401:
205
- this.page?.redirect('/');
205
+ this.page?.go('/');
206
206
  break;
207
207
  default:
208
208
  this.toast.error(e.title || "Uh Oh ...", e.message, null, { autohide: false });
@@ -225,12 +225,13 @@ export class ClientContext {
225
225
  }
226
226
  }
227
227
 
228
+ // Services
228
229
  public modal = createDialog(this, false);
229
230
  public toast = createDialog(this, true);
230
-
231
231
  public socket = new SocketClient(this)
232
232
  public captcha = new Recaptcha(this);
233
233
 
234
+ // Tracking
234
235
  public event( name: string, params?: object ) {
235
236
  if (!window.gtag) return;
236
237
  if (name === 'pageview')
@@ -33,7 +33,8 @@ declare global {
33
33
  interface Window {
34
34
  dev: boolean,
35
35
  context: ClientContext,
36
- user: User
36
+ user: User,
37
+ gtag: (action: string, name: string, params?: any) => void
37
38
  }
38
39
  }
39
40
 
@@ -69,10 +69,10 @@ export default () => {
69
69
 
70
70
  const [pages, setPages] = React.useState<{
71
71
  current: undefined | PageResponse,
72
- previous: undefined | PageResponse
72
+ //previous: undefined | PageResponse
73
73
  }>({
74
74
  current: gui.page,
75
- previous: undefined
75
+ //previous: undefined
76
76
  });
77
77
 
78
78
  const resolvePage = async (request: ClientRequest, locationUpdate?: Update) => {
@@ -116,13 +116,13 @@ export default () => {
116
116
  if (oldPage !== undefined) {
117
117
  setTimeout(() => setPages({
118
118
  current: newpage,
119
- previous: undefined
119
+ //previous: undefined
120
120
  }), 500);
121
121
  }
122
122
 
123
123
  return {
124
124
  current: newpage,
125
- previous: oldPage
125
+ //previous: oldPage
126
126
  }
127
127
  });
128
128
  }
@@ -175,9 +175,9 @@ export default () => {
175
175
 
176
176
  // Render the page component
177
177
  return <>
178
- {pages.previous && (
178
+ {/*pages.previous && (
179
179
  <Page page={pages.previous} key={pages.previous.id === undefined ? undefined : 'page_' + pages.previous.id} />
180
- )}
180
+ )*/}
181
181
 
182
182
  {pages.current && (
183
183
  <Page page={pages.current} isCurrent key={pages.current.id === undefined ? undefined : 'page_' + pages.current.id} />
@@ -35,23 +35,9 @@ export type TEnvConfig = {
35
35
  profile: 'dev' | 'prod',
36
36
  level: 'silly' | 'info' | 'warn' | 'error',
37
37
 
38
+ localIP: string,
38
39
  domain: string,
39
- protocol: 'http' | 'https',
40
- url: string, // protocol + domain
41
- localIP?: string
42
-
43
- http: {
44
- port: number,
45
- ssl: boolean
46
- },
47
-
48
- database: {
49
- host: string,
50
- port: number,
51
- login: string,
52
- password: string,
53
- list: string[]
54
- },
40
+ url: string,
55
41
  }
56
42
 
57
43
  type AppIdentityConfig = {
@@ -101,9 +87,27 @@ export default class ConfigParser {
101
87
  return yaml.parse(rawConfig);
102
88
  }
103
89
 
104
- public env() {
105
- const envFile = this.appDir + '/env' + (this.envName === undefined ? '' : '.' + this.envName) + '.yaml';
106
- return this.loadYaml( envFile );
90
+ public env(): TEnvConfig {
91
+ // We assume that when we run 5htp dev, we're in local
92
+ // Otherwise, we're in production environment (docker)
93
+ console.log("Using environment:", process.env.NODE_ENV);
94
+ return process.env.NODE_ENV === 'development' ? {
95
+ name: 'local',
96
+ profile: 'dev',
97
+ level: 'silly',
98
+
99
+ localIP: '86.76.176.80',
100
+ domain: 'localhost:3010',
101
+ url: 'http://localhost:3010',
102
+ } : {
103
+ name: 'server',
104
+ profile: 'prod',
105
+ level: 'silly',
106
+
107
+ localIP: '86.76.176.80',
108
+ domain: 'megacharger.io',
109
+ url: 'https://megacharger.io',
110
+ }
107
111
  }
108
112
 
109
113
  public identity() {
@@ -15,7 +15,7 @@ import ConfigParser, { TEnvConfig } from './config';
15
15
  ----------------------------------*/
16
16
 
17
17
  type THookName = 'ready' | 'cleanup' | 'error'
18
- type THook = () => void;
18
+ type THook = () => Promise<void>;
19
19
 
20
20
  type TServiceOptions = {
21
21
  instanciate: boolean
@@ -106,21 +106,42 @@ export class App {
106
106
  console.log("Configure services with", this.config);
107
107
  }
108
108
 
109
+ // Register a service
109
110
  public register( id: string, Service: TServiceClass, options: Partial<TServiceOptions> = {}) {
110
111
 
111
112
  // Pas d'export default new Service pour chaque fichier de service,
112
113
  // dissuaded'importer ms service sn'importe où, ce qui créé des références circulaires
113
- console.log(`Launching service ${id} ...`, Service);
114
+ console.log(`[services] Registering service ${id} ...`);
114
115
  const service = options.instanciate !== false ? new Service() : Service;
115
116
  this.services[id] = service;
116
117
 
117
- // Lorsque service.load est async, une propriété loading doit etre présente
118
- // De façon à ce que les autres services puissent savoir quand ce service est prêt
119
- if ('loading' in service) {
120
- service.loading = service.load();
121
- this.loading.push(service.loading);
122
- } else if ('load' in service)
123
- service.load();
118
+ if ('load' in service) {
119
+
120
+ console.log(`[services] Starting service ${id} ...`);
121
+ const loading = service.load()
122
+
123
+ // Lorsque service.load est async, une propriété loading doit etre présente
124
+ // De façon à ce que les autres services puissent savoir quand ce service est prêt
125
+ if (('loading' in service) && (loading instanceof Promise)) {
126
+
127
+ console.log(`[services] Waiting service ${id} to be fully loaded ...`);
128
+ service.loading = loading.then(() => {
129
+ console.info(`[service] Service ${id} successfully started.`);
130
+ }).catch(e => {
131
+ // Bug report via email
132
+ console.error(`[service] Error while starting the ${id} service:`, e);
133
+ e.message = `Start ${id} service: ` + e.message;
134
+ $.console.bugReport.server(e);
135
+ });;
136
+
137
+ this.loading.push(service.loading);
138
+ }
139
+ }
140
+ }
141
+
142
+ // Test if a service was registered
143
+ public isLoaded( id: string ) {
144
+ return id in this.services;
124
145
  }
125
146
 
126
147
  public on( name: THookName, callback: THook ) {
@@ -128,23 +149,36 @@ export class App {
128
149
  return this;
129
150
  }
130
151
 
152
+ public runHook( hookName: THookName ) {
153
+ console.info(`[hook] Run all ${hookName} hook (${this.hooks.ready.length}).`);
154
+ return Promise.all(
155
+ this.hooks.ready.map(
156
+ cb => cb().catch(e => {
157
+ console.error(`[hook] Error while executing hook ${hookName}:`, e);
158
+ })
159
+ )
160
+ ).then(() => {
161
+ console.info(`[hook] Hooks ${hookName} executed with success.`);
162
+ })
163
+ }
164
+
131
165
  /*----------------------------------
132
166
  - LAUNCH
133
167
  ----------------------------------*/
134
168
  public async launch() {
135
169
 
136
- console.info(`Waiting for services to be ready ...`);
170
+ console.info(`[boot] Waiting for all services to be ready ...`);
137
171
  await Promise.all( this.loading );
138
172
 
139
- console.info(`Launching application ...`);
140
- await Promise.all( this.hooks.ready.map(cb => cb()) );
173
+ console.info(`[boot] Launching application ...`);
174
+ await this.runHook('ready');
141
175
 
142
176
  // NOTE: Useless ?
143
177
  /*if (this.hmr)
144
178
  this.activateHMR();*/
145
179
 
146
- console.info(`Application is ready.`);
147
-
180
+ console.info(`[boot] Application is ready.`);
181
+
148
182
  this.launched = true;
149
183
 
150
184
  }
@@ -2,68 +2,123 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
+ // Node
6
+ import path from 'path';
7
+
5
8
  // Npm
6
9
  import hInterval from 'human-interval';
10
+ import fs from 'fs-extra';
11
+
12
+ // Core
13
+ import app from '@server/app';
7
14
 
8
15
  // Libs
9
- import redis from '@server/services/redis';
10
- import { ErreurCritique } from '@common/errors';
16
+
17
+ /*----------------------------------
18
+ - TYPES
19
+ ----------------------------------*/
20
+
21
+ type TPrimitiveValue = string | boolean | number | undefined | {[key: string]: TPrimitiveValue} | TPrimitiveValue[];
22
+
23
+ type TExpirationDelay = string | number | Date | null;
24
+
25
+ type CacheEntry = {
26
+ // Value
27
+ value: TPrimitiveValue,
28
+ // Expiration Timestamp
29
+ expiration?: number
30
+ };
11
31
 
12
32
  /*----------------------------------
13
33
  - SERVICE
14
34
  ----------------------------------*/
15
- const Cache = {
35
+ class Cache {
36
+
37
+ private cacheFile = path.join(app.path.data, 'cache/mem.json');
38
+
39
+ private data: {[key: string]: CacheEntry | undefined} = {};
40
+
41
+ private changes: number = 0;
42
+
43
+ public constructor() {
44
+
45
+ setInterval(() => this.cleanMem(), 10000);
46
+
47
+ if (fs.existsSync(this.cacheFile))
48
+ this.data = fs.readJSONSync(this.cacheFile)
49
+ }
50
+
51
+ private cleanMem() {
52
+
53
+ console.log("[cache] Clean memory");
54
+
55
+ const now = Date.now();
56
+ for (const key in this.data) {
57
+ const entry = this.data[ key ];
58
+ if (entry?.expiration && entry.expiration < now)
59
+ this.del(key);
60
+ }
61
+
62
+ if (this.changes > 0)
63
+ fs.outputJSONSync(this.cacheFile, this.data);
64
+ }
16
65
 
17
66
  // Expiration = Durée de vie en secondes ou date max
18
67
  // Retourne null quand pas de valeur
19
- async get<TValeur>(
68
+ public get<TValeur extends TPrimitiveValue>(
69
+ cle: string,
70
+ func?: (() => Promise<TValeur>),
71
+ expiration?: TExpirationDelay,
72
+ avecDetails?: true
73
+ ): Promise<CacheEntry>;
74
+
75
+ public get<TValeur extends TPrimitiveValue>(
76
+ cle: string,
77
+ func: (() => Promise<TValeur>),
78
+ expiration?: TExpirationDelay,
79
+ avecDetails?: false
80
+ ): Promise<null | TValeur>;
81
+
82
+ public async get<TValeur extends TPrimitiveValue>(
20
83
  cle: string,
21
- func: Function | null = null,
22
- expiration: number | Date | null = null,
84
+ func?: (() => Promise<TValeur>),
85
+ expiration?: TExpirationDelay,
23
86
  avecDetails?: boolean
24
- ): Promise<null | TValeur> {
87
+ ): Promise<null | TValeur | CacheEntry> {
88
+
89
+ let retour: CacheEntry | undefined = this.data[cle];
90
+
91
+ console.log(`[cache] Get "${cle}".`);
25
92
 
26
- let retour: any = await this.getVal(cle);
93
+ // Expired
94
+ if (retour?.expiration && retour.expiration < Date.now()){
95
+ console.log(`[cache] Key ${cle} expired.`);
96
+ retour = undefined;
97
+ }
27
98
 
28
99
  // Donnée inexistante
29
- if (retour === null && func !== null) {
100
+ if (retour === undefined && func !== undefined) {
30
101
 
31
102
  // Rechargement
32
- retour = await func();
103
+ retour = {
104
+ value: await func(),
105
+ expiration: expiration
106
+ ? this.delayToTimestamp(expiration)
107
+ : undefined
108
+ }
33
109
 
34
110
  // undefined retourné = pas d'enregistrement
35
- if (retour !== undefined)
111
+ if (retour.value !== undefined)
36
112
  await this.set(cle, retour, expiration);
37
113
  }
38
114
 
39
- return avecDetails
40
- ? {
41
- donnees: retour
42
- }
43
- : retour;
44
- },
45
-
46
- getVal<TValeur>(cle: string): Promise<TValeur | null> {
47
- return new Promise((resolve) => {
48
-
49
- redis.instance.get(cle, (err, val) => {
115
+ if (retour === undefined)
116
+ return null;
50
117
 
51
- if (val === null) {
52
- resolve( null );
53
- } else {
54
- try {
55
- resolve( JSON.parse(val) )
56
- } catch (error) {
57
-
58
- console.warn(`Error while parsing JSON value from cache (id: ${cle})`, error, 'Raw value:', val);
59
- resolve(null);
60
-
61
- }
62
- }
63
-
64
- });
65
- });
66
- },
118
+ return avecDetails
119
+ ? retour
120
+ : retour.value as TValeur;
121
+ };
67
122
 
68
123
  /**
69
124
  * Put in cache a JSON value, associated with an unique ID.
@@ -76,49 +131,44 @@ const Cache = {
76
131
  * - null: no expiration (default)
77
132
  * @returns A void promise
78
133
  */
79
- set( cle: string, val: any, expiration: string | number | Date | null = null ): Promise<void> {
80
- console.log("Updating cache " + cle);
81
- return new Promise((resolve) => {
82
-
83
- //console.info('Enregistrement de ' + cle + ' avec la valeur ', val, 'expiration', expiration);
84
-
85
- val = JSON.stringify(val);
134
+ public set( cle: string, val: TPrimitiveValue, expiration: TExpirationDelay = null ): void {
135
+
136
+ console.log("[cache] Updating cache " + cle);
137
+ this.data[ cle ] = {
138
+ value: val,
139
+ expiration: this.delayToTimestamp(expiration)
140
+ }
86
141
 
87
- // Conversion de l'expiration en nombre de secondes (ttl, time to live)
88
- if (expiration === null)
89
- redis.instance.set(cle, val, () => {
90
- resolve()
91
- });
92
- else {
142
+ this.changes++;
143
+ };
93
144
 
94
- let secondes: number;
145
+ public del( cle: string ): void {
146
+ this.data[ cle ] = undefined;
147
+ this.changes++;
148
+ }
95
149
 
96
- // H expression
97
- if (typeof expiration === 'string') {
98
150
 
99
- const ms = hInterval(expiration);
100
- if (ms === undefined) throw new Error(`Invalid period string: ` + expiration);
101
- secondes = ms / 1000;
151
+ /*----------------------------------
152
+ - UTILS
153
+ ----------------------------------*/
154
+ private delayToTimestamp( delay: TExpirationDelay ): number {
102
155
 
103
- // Via durée de vie en secondes
104
- } else if (typeof expiration === 'number')
105
- secondes = expiration;
106
- // Date limite
107
- else
108
- secondes = (expiration.getTime() - (new Date).getTime()) / 1000;
156
+ // H expression
157
+ if (typeof delay === 'string') {
109
158
 
110
- redis.instance.set(cle, val, 'EX', secondes, () => {
111
- resolve()
112
- });
113
- }
114
- });
115
- },
159
+ const ms = hInterval(delay);
160
+ if (ms === undefined) throw new Error(`Invalid period string: ` + delay);
161
+ return Date.now() + ms;
116
162
 
117
- del( cle: string ): Promise<void> {
118
- return new Promise((resolve) => {
119
- redis.instance.del(cle, () => resolve());
120
- });
163
+ // Via durée de vie en secondes
164
+ } else if (typeof delay === 'number')
165
+ return Date.now() + delay;
166
+ // Date limite
167
+ else if (delay !== null)
168
+ return delay.getTime();
169
+ else
170
+ return Date.now();
121
171
  }
122
172
  }
123
173
 
124
- export default Cache;
174
+ export default new Cache;
@@ -13,6 +13,9 @@ process.on('unhandledRejection', (error: any, promise: any) => {
13
13
 
14
14
  console.error("Unhandled promise rejection:", error);
15
15
 
16
+ // Send email report
17
+
18
+
16
19
  });
17
20
 
18
21
  /*----------------------------------
@@ -79,7 +79,6 @@ export default class BugReporter {
79
79
  // Get context
80
80
  const now = new Date();
81
81
  const hash = uuid();
82
- const erroTitle = "Server Bug: " + error.message;
83
82
  const { channelType, channelId } = this.console.getChannel();
84
83
 
85
84
  // On envoi l'email avant l'insertion dans bla bdd
@@ -90,31 +89,36 @@ export default class BugReporter {
90
89
  );
91
90
 
92
91
  // Send notification
93
- $.email.send({
94
- to: app.identity.author.email,
95
- subject: "Bug serveur: " + erroTitle,
96
- html: `
97
- <a href="${app.env.url}/admin/activity/requests/${channelId}">
98
- View Request details & console
99
- </a>
100
- <br/>
101
- ${logsHtml}
102
- `
103
- });
104
- // Memorize
105
- $.sql.insert('BugServer', {
106
- // Context
107
- hash: hash,
108
- date: now,
109
- channelType,
110
- channelId,
111
- // User
112
- user: request?.user?.name,
113
- ip: request?.ip,
114
- // Error
115
- stacktrace: error.stack || error.message,
116
- logs: logsHtml
117
- });
92
+ if (app.isLoaded('email'))
93
+ $.email.send({
94
+ to: app.identity.author.email,
95
+ subject: "Server bug: " + error.message,
96
+ html: `
97
+ <a href="${app.env.url}/admin/activity/requests/${channelId}">
98
+ View Request details & console
99
+ </a>
100
+ <br/>
101
+ ${logsHtml}
102
+ `
103
+ });
104
+ else
105
+ console.error("Unable to send bug report: email service not loaded.");
106
+
107
+ /*if (app.isLoaded('sql'))
108
+ // Memorize
109
+ $.sql.insert('BugServer', {
110
+ // Context
111
+ hash: hash,
112
+ date: now,
113
+ channelType,
114
+ channelId,
115
+ // User
116
+ user: request?.user?.name,
117
+ ip: request?.ip,
118
+ // Error
119
+ stacktrace: error.stack || error.message,
120
+ logs: logsHtml
121
+ });*/
118
122
 
119
123
  // Update error message
120
124
  error.message = "A bug report has been sent to my personal mailbox. Sorry for the inconvenience.";
File without changes
@@ -256,7 +256,7 @@ export class Console {
256
256
  let html = logs.map( log => logToHTML( log, this )).join('\n');
257
257
 
258
258
  if (full) {
259
- const consoleCss = `background: #000; padding: 20px; font-family: 'monospace'; font-size: 12px; line-height: 20px;`
259
+ const consoleCss = `background: #000; padding: 20px; font-family: 'Fira Mono', 'monospace', 'Monaco'; font-size: 12px; line-height: 20px;`
260
260
  html = '<div style="' + consoleCss + '">' + html + '</div>';
261
261
  }
262
262
 
@@ -62,13 +62,15 @@ export default class FastDatabase {
62
62
 
63
63
  console.info(`Connecting to databases ...`);
64
64
 
65
+ const creds = this.config[ app.env.profile ];
66
+
65
67
  return await mysql.createPool({
66
68
 
67
69
  // Identification
68
- host: this.config.host,
69
- port: this.config.port,
70
- user: this.config.login,
71
- password: this.config.password,
70
+ host: creds.host,
71
+ port: creds.port,
72
+ user: creds.login,
73
+ password: creds.password,
72
74
  database: this.config.list[0],
73
75
 
74
76
  // Pool
@@ -106,7 +108,7 @@ export default class FastDatabase {
106
108
  let type = field.type;
107
109
  if (field.db) {
108
110
 
109
- // A revoir, car les infos passées peuvent être des alias
111
+ // TODO: A revoir, car les infos passées peuvent être des alias
110
112
 
111
113
  const db = this.tables[ field.db ];
112
114
  if (db === undefined) {
@@ -41,11 +41,19 @@ export type TInsertQueryOptions<TData extends TObjetDonnees = TObjetDonnees> = T
41
41
  ----------------------------------*/
42
42
 
43
43
  export type DatabaseServiceConfig = {
44
- host: string,
45
44
  list: string[],
46
- login: string,
47
- password: string,
48
- port: number
45
+ dev: {
46
+ host: string,
47
+ port: number,
48
+ login: string,
49
+ password: string,
50
+ },
51
+ prod: {
52
+ host: string,
53
+ port: number,
54
+ login: string,
55
+ password: string,
56
+ }
49
57
  }
50
58
 
51
59
  declare global {
@@ -89,7 +89,7 @@ export default class HttpServer {
89
89
 
90
90
  this.http = http.createServer(this.express);
91
91
 
92
- } else if ('ssh' in app.env) {
92
+ } /*else if ('ssh' in app.env) {
93
93
 
94
94
  const ssh = app.env.ssh;
95
95
 
@@ -102,7 +102,7 @@ export default class HttpServer {
102
102
  rejectUnauthorized: false
103
103
  }, this.express);
104
104
 
105
- } else
105
+ }*/ else
106
106
  throw new Error(`SSL was enabled, but no ssh config was specified in app.env (required to load ssl certificate files)`);
107
107
 
108
108
  // Start HTTP Server
@@ -49,7 +49,7 @@ export class WebSocketCommander {
49
49
  public scopes: {[path: string]: SocketScope} = {}
50
50
 
51
51
  public constructor() {
52
- app.on('cleanup', () => {
52
+ app.on('cleanup', async () => {
53
53
  this.closeAll();
54
54
  });
55
55
  }
@@ -34,9 +34,9 @@
34
34
  "@errors": ["./common/errors"],
35
35
  "@models": ["./common/models"],
36
36
 
37
- "react": ["../node_modules/preact/compat"],
38
- "react-dom": ["../node_modules/preact/compat"],
39
- "react/jsx-runtime": ["../node_modules/preact/jsx-runtime"]
37
+ "react": ["preact/compat"],
38
+ "react-dom": ["preact/compat"],
39
+ "react/jsx-runtime": ["preact/jsx-runtime"]
40
40
  },
41
41
  },
42
42
  "include": ["src"]