11import 'dotenv/config'
2- import express from 'express' ;
2+ import express , { Express } from 'express' ;
33import rateLimit from 'express-rate-limit' ;
44import bodyParser from 'body-parser' ;
55import cors from 'cors' ;
66import path , { dirname } from 'path' ;
7+ import { fileURLToPath } from 'url' ;
8+ import * as http from 'http' ;
9+ import { AddressInfo } from 'net' ;
710import apiRoutes from "./routes/index.js"
8- import { dbConnect } from './database.js' ;
9- import setup from './services/setup.js' ;
10- import settingsService from './services/settings.service.js' ;
11- import SmeeService from './services/smee.js' ;
11+ import Database from './database.js' ;
1212import logger , { expressLoggerMiddleware } from './services/logger.js' ;
13- import { fileURLToPath } from 'url' ;
13+ import GitHub from './github.js' ;
14+ import WebhookService from './services/smee.js' ;
15+ import SettingsService from './services/settings.service.js' ;
16+ import whyIsNodeRunning from 'why-is-node-running' ;
17+
18+ class App {
19+ eListener ?: http . Server ;
20+ baseUrl ?: string ;
1421
15- const PORT = Number ( process . env . PORT ) || 80 ;
16-
17- export const app = express ( ) ;
18- app . use ( cors ( ) ) ;
19- app . use ( expressLoggerMiddleware ) ;
20-
21- ( async ( ) => {
22- await dbConnect ( ) ;
23- logger . info ( 'DB Connected ✅' ) ;
24- await settingsService . initializeSettings ( ) ;
25- logger . info ( 'Settings loaded ✅' ) ;
26- await SmeeService . createSmeeWebhookProxy ( PORT ) ;
27- logger . info ( 'Created Smee webhook proxy ✅' ) ;
28-
29- try {
30- await setup . createAppFromEnv ( ) ;
31- logger . info ( 'Created GitHub App from environment ✅' ) ;
32- } catch ( error ) {
33- logger . info ( 'Failed to create app from environment. This is expected if the app is not yet installed.' , error ) ;
22+ constructor (
23+ public e : Express ,
24+ public port : number ,
25+ public database : Database ,
26+ public github : GitHub ,
27+ public settingsService : SettingsService
28+ ) {
29+ this . e = e ;
30+ this . port = port ;
3431 }
3532
36- app . use ( ( req , res , next ) => {
37- if ( req . path === '/api/github/webhooks' ) {
38- return next ( ) ;
39- }
40- bodyParser . json ( ) ( req , res , next ) ;
41- } , bodyParser . urlencoded ( { extended : true } ) ) ;
42- app . use ( '/api' , apiRoutes ) ;
43-
44- const __filename = fileURLToPath ( import . meta. url ) ;
45- const __dirname = dirname ( __filename ) ;
46- const frontendPath = path . resolve ( __dirname , '../../frontend/dist/github-value/browser' ) ;
47-
48- app . use ( express . static ( frontendPath ) ) ;
49- app . get ( '*' , rateLimit ( {
50- windowMs : 15 * 60 * 1000 , max : 5000 ,
51- } ) , ( _ , res ) => res . sendFile ( path . join ( frontendPath , 'index.html' ) ) ) ;
52-
53- app . listen ( PORT , ( ) => {
54- logger . info ( `Server is running at http://localhost:${ PORT } 🚀` ) ;
55- if ( process . env . WEB_URL ) {
56- logger . debug ( `Frontend is running at ${ process . env . WEB_URL } 🚀` ) ;
33+ public async start ( ) {
34+ try {
35+ this . setupExpress ( ) ;
36+ await this . database . connect ( ) ;
37+
38+ await this . initializeSettings ( ) ;
39+ logger . info ( 'Settings initialized' ) ;
40+
41+ await this . github . connect ( ) ;
42+ logger . info ( 'Created GitHub App from environment' ) ;
43+
44+ return this . e ;
45+ } catch ( error ) {
46+ await this . github . smee . connect ( ) ;
47+ logger . debug ( error ) ;
48+ logger . error ( 'Failed to start application ❌' ) ;
49+ if ( error instanceof Error ) {
50+ logger . error ( error . message ) ;
51+ }
5752 }
53+ }
54+
55+ public stop ( ) {
56+ whyIsNodeRunning ( )
57+ this . database . disconnect ( ) ;
58+ this . github . disconnect ( ) ;
59+ this . eListener ?. close ( ( ) => {
60+ logger . info ( 'Server closed' ) ;
61+ process . exit ( 0 ) ;
62+ } ) ;
63+ }
64+
65+ private setupExpress ( ) {
66+ this . e . use ( cors ( ) ) ;
67+ this . e . use ( expressLoggerMiddleware ) ;
68+ this . e . use ( ( req , res , next ) => {
69+ if ( req . path === '/api/github/webhooks' ) {
70+ return next ( ) ;
71+ }
72+ bodyParser . json ( ) ( req , res , next ) ;
73+ } , bodyParser . urlencoded ( { extended : true } ) ) ;
74+
75+ this . e . use ( '/api' , apiRoutes ) ;
76+
77+ const __filename = fileURLToPath ( import . meta. url ) ;
78+ const __dirname = dirname ( __filename ) ;
79+ const frontendPath = path . resolve ( __dirname , '../../frontend/dist/github-value/browser' ) ;
80+ this . e . use ( express . static ( frontendPath ) ) ;
81+ this . e . get ( '*' , rateLimit ( {
82+ windowMs : 15 * 60 * 1000 , max : 5000 ,
83+ } ) , ( _ , res ) => res . sendFile ( path . join ( frontendPath , 'index.html' ) ) ) ;
84+
85+ const listener = this . e . listen ( this . port , ( ) => {
86+ const address = listener . address ( ) as AddressInfo ;
87+ logger . info ( `Server is running at http://${ address . address === '::' ? 'localhost' : address . address } :${ address . port } 🚀` ) ;
88+ } ) ;
89+ this . eListener = listener ;
90+ }
91+
92+ private initializeSettings ( ) {
93+ this . settingsService . initialize ( )
94+ . then ( async ( settings ) => {
95+ if ( settings . webhookProxyUrl ) {
96+ this . github . smee . options . url = settings . webhookProxyUrl
97+ }
98+ if ( settings . webhookSecret ) {
99+ this . github . setInput ( {
100+ webhooks : {
101+ secret : settings . webhookSecret
102+ }
103+ } ) ;
104+ }
105+ if ( settings . metricsCronExpression ) {
106+ this . github . cronExpression = settings . metricsCronExpression ;
107+ }
108+ if ( settings . baseUrl ) {
109+ this . baseUrl = settings . baseUrl ;
110+ }
111+ } )
112+ . finally ( async ( ) => {
113+ await this . github . smee . connect ( )
114+ await this . settingsService . updateSetting ( 'webhookSecret' , this . github . input . webhooks ?. secret || '' ) ;
115+ await this . settingsService . updateSetting ( 'webhookProxyUrl' , this . github . smee . options . url ! ) ;
116+ await this . settingsService . updateSetting ( 'metricsCronExpression' , this . github . cronExpression ! ) ;
117+ } ) ;
118+ }
119+ }
120+
121+ const port = Number ( process . env . PORT ) || 80 ;
122+ const e = express ( ) ;
123+ const app = new App (
124+ e ,
125+ port ,
126+ new Database ( {
127+ dialect : 'mysql' ,
128+ logging : ( sql ) => logger . debug ( sql ) ,
129+ timezone : '+00:00' , // Force UTC timezone
130+ dialectOptions : {
131+ timezone : '+00:00' // Force UTC for MySQL connection
132+ } ,
133+ host : process . env . MYSQL_HOST ,
134+ port : Number ( process . env . MYSQL_PORT ) || 3306 ,
135+ username : process . env . MYSQL_USER ,
136+ password : process . env . MYSQL_PASSWORD ,
137+ database : process . env . MYSQL_DATABASE || 'value'
138+ } ) ,
139+ new GitHub (
140+ {
141+ appId : process . env . GITHUB_APP_ID ,
142+ privateKey : process . env . GITHUB_APP_PRIVATE_KEY ,
143+ webhooks : {
144+ secret : process . env . GITHUB_WEBHOOK_SECRET
145+ }
146+ } ,
147+ e ,
148+ new WebhookService ( {
149+ url : process . env . WEBHOOK_PROXY_URL ,
150+ path : '/api/github/webhooks' ,
151+ port
152+ } )
153+ ) , new SettingsService ( {
154+ baseUrl : process . env . BASE_URL ,
155+ webhookProxyUrl : process . env . GITHUB_WEBHOOK_PROXY_URL ,
156+ webhookSecret : process . env . GITHUB_WEBHOOK_SECRET ,
157+ metricsCronExpression : '0 0 * * *' ,
158+ devCostPerYear : '100000' ,
159+ developerCount : '100' ,
160+ hoursPerYear : '2080' ,
161+ percentTimeSaved : '20' ,
162+ percentCoding : '20'
163+ } )
164+ ) ;
165+ app . start ( ) ;
166+ logger . info ( 'App started' ) ;
167+
168+ export default app ;
169+
170+ [ 'SIGTERM' , 'SIGINT' , 'SIGQUIT' ] . forEach ( signal => {
171+ process . on ( signal , ( ) => {
172+ logger . info ( `Received ${ signal } . Stopping the app...` ) ;
173+ app . stop ( ) ;
174+ process . exit ( signal === 'uncaughtException' ? 1 : 0 ) ;
58175 } ) ;
59- } ) ( ) ;
176+ } ) ;
0 commit comments