@agung_dhewe/webapps 1.1.2
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/LICENSE +28 -0
- package/README.md +2 -0
- package/jsconfig.json +10 -0
- package/libs/fgta5js-dist/fgta5js-v1.8.3.min.css +2 -0
- package/libs/fgta5js-dist/fgta5js-v1.8.3.min.js +11 -0
- package/libs/fgta5js-dist/fgta5js-v1.8.3.min.js.map +1 -0
- package/libs/fgta5js-dist/fonts/karla-italic-latin-ext.woff2 +0 -0
- package/libs/fgta5js-dist/fonts/karla-italic-latin.woff2 +0 -0
- package/libs/fgta5js-dist/fonts/karla-normal-latin-ext.woff2 +0 -0
- package/libs/fgta5js-dist/fonts/karla-normal-latin.woff2 +0 -0
- package/libs/fgta5js-dist/fonts/karla.css +142 -0
- package/libs/webmodule/module-edit.css +163 -0
- package/libs/webmodule/module-footer.css +22 -0
- package/libs/webmodule/module-list.css +25 -0
- package/libs/webmodule/module.css +52 -0
- package/libs/webmodule/module.js +195 -0
- package/libs/webmodule/pagehelper.mjs +45 -0
- package/modules/generator/appgen-components.mjs +142 -0
- package/modules/generator/appgen-icons.mjs +6 -0
- package/modules/generator/appgen-io.mjs +784 -0
- package/modules/generator/appgen-ui-search.mjs +173 -0
- package/modules/generator/appgen-ui-unique.mjs +153 -0
- package/modules/generator/appgen-ui.mjs +1181 -0
- package/modules/generator/generator-context.mjs +18 -0
- package/modules/generator/generator-designtemplate.html +1508 -0
- package/modules/generator/generator-ext.html +0 -0
- package/modules/generator/generator-ext.mjs +3 -0
- package/modules/generator/generator.css +642 -0
- package/modules/generator/generator.mjs +195 -0
- package/modules/generator/generator.png +0 -0
- package/modules/generator/generatorEdit.html +185 -0
- package/modules/generator/generatorEdit.mjs +238 -0
- package/modules/generator/generatorList.html +32 -0
- package/modules/generator/generatorList.mjs +243 -0
- package/modules/login/login.css +11 -0
- package/modules/login/login.html +12 -0
- package/modules/login/login.mjs +111 -0
- package/package.json +46 -0
- package/percobaan/simmpan-ke-minio.js +24 -0
- package/src/api.js +80 -0
- package/src/apis/generator.api.js +226 -0
- package/src/apis/login.api.js +109 -0
- package/src/bucket.js +24 -0
- package/src/context.js +26 -0
- package/src/datalog.sql +22 -0
- package/src/datarecords.js +0 -0
- package/src/db.js +61 -0
- package/src/generator/createApiExtenderModule.js +54 -0
- package/src/generator/createApiModule.js +218 -0
- package/src/generator/createIcon.js +62 -0
- package/src/generator/createInfoAboutExtender.js +42 -0
- package/src/generator/createInfoLogs.js +41 -0
- package/src/generator/createInfoRecordExtender.js +41 -0
- package/src/generator/createModuleContext.js +48 -0
- package/src/generator/createModuleDetilEditHtml.js +110 -0
- package/src/generator/createModuleDetilEditMjs.js +172 -0
- package/src/generator/createModuleDetilListHtml.js +146 -0
- package/src/generator/createModuleDetilListMjs.js +73 -0
- package/src/generator/createModuleEjs.js +51 -0
- package/src/generator/createModuleExtenderHtml.js +43 -0
- package/src/generator/createModuleExtenderMjs.js +43 -0
- package/src/generator/createModuleHeaderEditHtml.js +148 -0
- package/src/generator/createModuleHeaderEditMjs.js +197 -0
- package/src/generator/createModuleHeaderListHtml.js +144 -0
- package/src/generator/createModuleHeaderListMjs.js +67 -0
- package/src/generator/createModuleMjs.js +67 -0
- package/src/generator/createModuleRollup.js +42 -0
- package/src/generator/createProgramData.js +96 -0
- package/src/generator/createTable.js +156 -0
- package/src/generator/ddl.js +475 -0
- package/src/generator/helper.js +149 -0
- package/src/generator/templates/__rollup-module.ejs +90 -0
- package/src/generator/templates/api-extender-module.js.ejs +0 -0
- package/src/generator/templates/api-module.js.ejs +818 -0
- package/src/generator/templates/module-context.ejs +16 -0
- package/src/generator/templates/module-ext-about.ejs +1 -0
- package/src/generator/templates/module-ext-record.ejs +1 -0
- package/src/generator/templates/module-ext.html.ejs +3 -0
- package/src/generator/templates/module-ext.mjs.ejs +21 -0
- package/src/generator/templates/module-logs.ejs +14 -0
- package/src/generator/templates/module.ejs.ejs +48 -0
- package/src/generator/templates/module.mjs.ejs +256 -0
- package/src/generator/templates/moduleDetilEdit.html.ejs +34 -0
- package/src/generator/templates/moduleDetilEdit.mjs.ejs +792 -0
- package/src/generator/templates/moduleDetilList.html.ejs +26 -0
- package/src/generator/templates/moduleDetilList.mjs.ejs +319 -0
- package/src/generator/templates/moduleHeaderEdit.html.ejs +53 -0
- package/src/generator/templates/moduleHeaderEdit.mjs.ejs +807 -0
- package/src/generator/templates/moduleHeaderList.html.ejs +24 -0
- package/src/generator/templates/moduleHeaderList.mjs.ejs +308 -0
- package/src/generator/templates/sqlAddField.ejs +3 -0
- package/src/generator/templates/sqlAddForeignKey.ejs +12 -0
- package/src/generator/templates/sqlAddUniqueIndex.ejs +4 -0
- package/src/generator/templates/sqlCreateTable.ejs +9 -0
- package/src/generator/templates/sqlDropForeignKey.ejs +3 -0
- package/src/generator/templates/sqlDropUniqueIndex.ejs +4 -0
- package/src/generator/templates/sqlModifyField.ejs +6 -0
- package/src/generator/trygenerate.js +83 -0
- package/src/generator/worker.js +389 -0
- package/src/helper.js +82 -0
- package/src/logger.js +39 -0
- package/src/router.js +84 -0
- package/src/routers/defaultLoginApi.js +29 -0
- package/src/routers/defaultLoginAsset.js +18 -0
- package/src/routers/defaultLoginPage.js +36 -0
- package/src/routers/defaultRootIndex.js +16 -0
- package/src/routers/downloadHandler.js +51 -0
- package/src/routers/fileUploadApi.js +15 -0
- package/src/routers/generatorApi.js +30 -0
- package/src/routers/generatorAsset.js +18 -0
- package/src/routers/generatorPage.js +37 -0
- package/src/routers/handleError.js +43 -0
- package/src/routers/handleModuleNotfound.js +12 -0
- package/src/routers/moduleApi.js +34 -0
- package/src/routers/modulePage.js +102 -0
- package/src/sequencerdoc.js +311 -0
- package/src/sequencerline.js +214 -0
- package/src/session.js +57 -0
- package/src/startup.js +59 -0
- package/src/webapps.js +239 -0
- package/src/workermanager.js +83 -0
- package/templates/_lib_debug.ejs +11 -0
- package/templates/_lib_production.ejs +5 -0
- package/templates/application.page.ejs +143 -0
- package/templates/generator.page.ejs +131 -0
- package/templates/index.page.ejs +24 -0
- package/templates/login.page.ejs +102 -0
- package/templates/moduleError.ejs +16 -0
- package/templates/moduleNotfound.ejs +14 -0
- package/webapps.code-workspace +11 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import sqlUtil from '@agung_dhewe/pgsqlc'
|
|
2
|
+
|
|
3
|
+
const MAX_LENGTH = 12
|
|
4
|
+
const tablename = "core.sequencer_line"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function createSequencerLine(db, options) {
|
|
8
|
+
return new Sequencer(db, options)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Sequencer {
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#defaultOptions = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
db
|
|
19
|
+
options
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
constructor(db, opt) {
|
|
23
|
+
this.db = db
|
|
24
|
+
this.options = {
|
|
25
|
+
...this.#defaultOptions,
|
|
26
|
+
...opt
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async increment(doc_id) {
|
|
36
|
+
const self = this
|
|
37
|
+
const db = self.db
|
|
38
|
+
try {
|
|
39
|
+
const { year, month } = await getDbCurrentDate(db)
|
|
40
|
+
return await generateId(self, year, month, doc_id)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw err
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 9 digit, untuk tipe integer
|
|
47
|
+
async yearlyshort(doc_id) {
|
|
48
|
+
const self = this
|
|
49
|
+
const db = self.db
|
|
50
|
+
try {
|
|
51
|
+
const month = 0
|
|
52
|
+
const { year } = await getDbCurrentDate(db)
|
|
53
|
+
return await generateId(self, year, month, doc_id, true)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw err
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async function generateId(self, year, month, doc_id, short=false) {
|
|
63
|
+
const options = self.options
|
|
64
|
+
const db = self.db
|
|
65
|
+
const searchMap = {
|
|
66
|
+
seqnum: `sequencer_seqnum=\${seqnum}`,
|
|
67
|
+
year: `sequencer_year=\${year}`,
|
|
68
|
+
month: `sequencer_month=\${month}`
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
sqlUtil.connect(db)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
|
|
75
|
+
// ambil code doc
|
|
76
|
+
const docparam = { doc_id }
|
|
77
|
+
const sqldoc = `select doc_seqnum from core.doc where doc_id=\${doc_id}`
|
|
78
|
+
const row = await db.oneOrNone(sqldoc, docparam)
|
|
79
|
+
const seqnum = row!=null ? row.doc_seqnum : 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if (seqnum<0 || seqnum>99) {
|
|
83
|
+
throw new Error(`doc_id: '${doc_id}' has doc seqnum '${seqnum}' that is invalid. doc seqnum have to in range 1-99 `)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// hitung total panjang bigint yang akan dihailkan
|
|
89
|
+
let nlength = 0
|
|
90
|
+
if (month==0) {
|
|
91
|
+
// yearly
|
|
92
|
+
nlength += 2
|
|
93
|
+
} else {
|
|
94
|
+
// montly
|
|
95
|
+
nlength += 4
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (seqnum>0) {
|
|
99
|
+
nlength += 2
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
// 25 09 01 000402 0000009
|
|
105
|
+
|
|
106
|
+
const ln = nlength + self.options.numberLength
|
|
107
|
+
|
|
108
|
+
if (short) {
|
|
109
|
+
if (ln > 9) {
|
|
110
|
+
throw new Error(`Total length of sequencer (${ln}) is mre than max length allowed(9)`)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
if (ln > MAX_LENGTH) {
|
|
114
|
+
throw new Error(`Total length of sequencer (${ln}) is mre than max length allowed(${MAX_LENGTH})`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// ambil data sequencer
|
|
120
|
+
{
|
|
121
|
+
const criteria = { year, month, seqnum }
|
|
122
|
+
const {whereClause, queryParams} = sqlUtil.createWhereClause(criteria, searchMap)
|
|
123
|
+
|
|
124
|
+
const columns = [
|
|
125
|
+
'sequencer_id',
|
|
126
|
+
'sequencer_year',
|
|
127
|
+
'sequencer_month',
|
|
128
|
+
'sequencer_seqnum',
|
|
129
|
+
'sequencer_number',
|
|
130
|
+
'EXTRACT(YEAR FROM sequencer_lastdate) AS lastyear',
|
|
131
|
+
'EXTRACT(MONTH FROM sequencer_lastdate) AS lastmonth'
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const sql = sqlUtil.createSqlSelect({tablename, columns, whereClause, sort:{}, limit:0, offset:0, queryParams}) + ' for update'
|
|
135
|
+
const row = await db.oneOrNone(sql, queryParams)
|
|
136
|
+
|
|
137
|
+
const obj = {}
|
|
138
|
+
|
|
139
|
+
if (row!=null) {
|
|
140
|
+
// console.log(row)
|
|
141
|
+
obj.sequencer_id = row.sequencer_id
|
|
142
|
+
obj._modifyby = 0
|
|
143
|
+
obj._modifydate = (new Date()).toISOString()
|
|
144
|
+
obj.sequencer_number = row.sequencer_number+1
|
|
145
|
+
const cmd = sqlUtil.createUpdateCommand(tablename, obj, ['sequencer_id'])
|
|
146
|
+
await cmd.execute(obj)
|
|
147
|
+
} else {
|
|
148
|
+
obj._createby = 0
|
|
149
|
+
obj._createdate = (new Date()).toISOString()
|
|
150
|
+
obj.sequencer_year = year,
|
|
151
|
+
obj.sequencer_month = month
|
|
152
|
+
obj.sequencer_seqnum = seqnum
|
|
153
|
+
obj.sequencer_number = 1
|
|
154
|
+
obj.sequencer_lastdate = (new Date()).toISOString()
|
|
155
|
+
obj.sequencer_remark = doc_id
|
|
156
|
+
const cmd = sqlUtil.createInsertCommand(tablename, obj, ['sequencer_id'])
|
|
157
|
+
await cmd.execute(obj)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// compose generated id
|
|
161
|
+
const YY = String(year-2000).padStart(2, '0')
|
|
162
|
+
const MM = String(month).padStart(2, '0')
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
const tokennum = []
|
|
166
|
+
if (short) {
|
|
167
|
+
// hanya tahun
|
|
168
|
+
tokennum.push(YY)
|
|
169
|
+
} else {
|
|
170
|
+
// tahun dan bulan
|
|
171
|
+
tokennum.push(YY)
|
|
172
|
+
tokennum.push(MM)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if (seqnum>0) {
|
|
178
|
+
tokennum.push(String(seqnum).padStart(2, '0'))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
// 250901999999
|
|
183
|
+
// 2509019999999
|
|
184
|
+
const maxlen = short ? 9 : MAX_LENGTH
|
|
185
|
+
const idpref = tokennum.join('')
|
|
186
|
+
const numlen = maxlen - idpref.length
|
|
187
|
+
tokennum.push(String(obj.sequencer_number).padStart(numlen, '0'))
|
|
188
|
+
|
|
189
|
+
const s = tokennum.join('')
|
|
190
|
+
return s
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
throw err
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function getDbCurrentDate(db) {
|
|
198
|
+
try {
|
|
199
|
+
const sql = "SELECT EXTRACT('year' FROM CURRENT_DATE) AS year, EXTRACT('month' FROM CURRENT_DATE) AS month"
|
|
200
|
+
const row = await db.one(sql)
|
|
201
|
+
|
|
202
|
+
const ret = {
|
|
203
|
+
year: row.year,
|
|
204
|
+
month: row.month
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return ret
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw err
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
package/src/session.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import session from 'express-session'; // session
|
|
2
|
+
import { createClient } from 'redis'; // session
|
|
3
|
+
import * as connectRedis from 'connect-redis'; // session
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function createSession(options) {
|
|
7
|
+
|
|
8
|
+
const redisUrl = options.redisUrl || 'redis://localhost:6379'
|
|
9
|
+
const sessionName = options.sessionName || 'sid'
|
|
10
|
+
const sessionSecret = options.sessionSecret || 'rahasia'
|
|
11
|
+
const sessionMaxAge = options.sessionMaxAge || 15 * 50 * 1000 // default 15 menit
|
|
12
|
+
const sessionDomain = options.sessionDomain || 'localhost'
|
|
13
|
+
const sessionSecure = options.sessionSecure ?? false
|
|
14
|
+
const sessionHttpOnly = options.sessionHttpOnly ?? true
|
|
15
|
+
|
|
16
|
+
const RedisStore = connectRedis.RedisStore;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
console.log(`connecting to redis ${redisUrl}`)
|
|
20
|
+
const redisClient = createClient({
|
|
21
|
+
url: redisUrl
|
|
22
|
+
});
|
|
23
|
+
await redisClient.connect();
|
|
24
|
+
console.log('connected to redis server.')
|
|
25
|
+
|
|
26
|
+
const redisStore = new RedisStore({
|
|
27
|
+
client: redisClient,
|
|
28
|
+
prefix: 'sess:',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const sessionConfig = {
|
|
33
|
+
name: sessionName,
|
|
34
|
+
store: redisStore,
|
|
35
|
+
secret: sessionSecret,
|
|
36
|
+
resave: false,
|
|
37
|
+
saveUninitialized: false,
|
|
38
|
+
rolling: true,
|
|
39
|
+
cookie: {
|
|
40
|
+
secure: sessionSecure,
|
|
41
|
+
httpOnly: sessionHttpOnly,
|
|
42
|
+
maxAge: sessionMaxAge,
|
|
43
|
+
domain: sessionDomain
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// TODO: tambahkan untuk keperluan ini
|
|
48
|
+
// domain: '.example.com', // ✅ Cookie tersedia untuk semua subdomain
|
|
49
|
+
// path: '/', // ✅ Berlaku untuk semua path
|
|
50
|
+
// secure: true, // ✅ Hanya dikirim lewat HTTPS
|
|
51
|
+
// httpOnly: true, // ✅ Tidak bisa diakses dari JavaScript
|
|
52
|
+
// sameSite: 'none', // ✅ Bisa lintas domain (harus paired dengan secure)
|
|
53
|
+
// maxAge: 15 * MINUTE,
|
|
54
|
+
|
|
55
|
+
console.log('starting session manager.')
|
|
56
|
+
return session(sessionConfig)
|
|
57
|
+
}
|
package/src/startup.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
export async function authorizeRequest(db, req) {
|
|
3
|
+
const moduleName = req.params.modulename;
|
|
4
|
+
const program_id = req.query.prog;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
|
|
8
|
+
// jika belum login
|
|
9
|
+
if (req.session.user==null) {
|
|
10
|
+
const err = new Error(`Belum login. Anda harus <a href="login">login</a> dulu untuk mengakses resource ini`)
|
|
11
|
+
err.status = 401
|
|
12
|
+
err.code = 401
|
|
13
|
+
throw err
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const user_id = req.session.user.userId
|
|
17
|
+
const user_fullname = req.session.user.userFullname
|
|
18
|
+
const developerAccess = req.session.user.developerAccess
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// jika punya akses developer boleh buka semuanya
|
|
22
|
+
const sqlUser = 'select * from core.user where user_id=${user_id} and user_isdev=true'
|
|
23
|
+
const rowUser = await db.oneOrNone(sqlUser, {user_id})
|
|
24
|
+
if (rowUser!=null) {
|
|
25
|
+
return true // user adalah developer
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// jika tidak punya akses developer, cek apakah boleh buka program
|
|
29
|
+
const sql = 'select * from core.get_user_programs(${user_id}) where id=${program_id}'
|
|
30
|
+
const row = await db.oneOrNone(sql, {user_id, program_id});
|
|
31
|
+
if (row!=null) {
|
|
32
|
+
return true // user punya akses program
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const err = new Error(`user '${user_fullname}' tidak diperbolehkan mengakses program '${moduleName}'`)
|
|
36
|
+
err.status = 401
|
|
37
|
+
err.code = 401
|
|
38
|
+
throw err
|
|
39
|
+
|
|
40
|
+
} catch(err) {
|
|
41
|
+
throw err
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getApplicationSetting(db) {
|
|
46
|
+
const setting = {}
|
|
47
|
+
try {
|
|
48
|
+
const sql = 'select setting_id, setting_value from core.setting'
|
|
49
|
+
const rows = await db.any(sql);
|
|
50
|
+
for (var row of rows) {
|
|
51
|
+
const setting_id = row.setting_id
|
|
52
|
+
const setting_value = row.setting_value
|
|
53
|
+
setting[setting_id] = setting_value
|
|
54
|
+
}
|
|
55
|
+
return setting
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/webapps.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import ExpressServer from 'express';
|
|
2
|
+
import { createSession } from './session.js'
|
|
3
|
+
import context from './context.js'
|
|
4
|
+
import { createBasicRouter, uploader } from './router.js'
|
|
5
|
+
import { handleModuleNotfound } from './routers/handleModuleNotfound.js'
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import cors from 'cors';
|
|
8
|
+
import favicon from 'serve-favicon';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as helper from './helper.js'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const defaultPort = 3000
|
|
20
|
+
const defaultRedisUrl = 'redis://localhost:6379'
|
|
21
|
+
const defaultSessionName = 'sid'
|
|
22
|
+
const defaultSessionSecret = 'rahasia'
|
|
23
|
+
const defaultSessionMaxAge = 15
|
|
24
|
+
const defaultSessionDomain = 'localhost'
|
|
25
|
+
const defaultSessionSecure = false
|
|
26
|
+
const defaultSessionHttpOnly = true
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const defaultNotifierSocket = 'ws://localhost:8080'
|
|
30
|
+
const defaultNotifierServer = 'http://localhost:8080'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export function createWebApplication() {
|
|
34
|
+
const webapp = new WebApplication()
|
|
35
|
+
return webapp
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export default class WebApplication {
|
|
40
|
+
#startedOnce
|
|
41
|
+
#express = ExpressServer();
|
|
42
|
+
|
|
43
|
+
get defaultPort() { return defaultPort }
|
|
44
|
+
get defaultRedisUrl() { return defaultRedisUrl }
|
|
45
|
+
get defaultNotifierSocket() { return defaultNotifierSocket }
|
|
46
|
+
get defaultNotifierServer() { return defaultNotifierServer }
|
|
47
|
+
get defaultSessionName() { return defaultSessionName }
|
|
48
|
+
get defaultSessionSecret() { return defaultSessionSecret }
|
|
49
|
+
get defaultSessionMaxAge() { return defaultSessionMaxAge }
|
|
50
|
+
get defaultSessionDomain() { return defaultSessionDomain }
|
|
51
|
+
get defaultSessionSecure() { return defaultSessionSecure }
|
|
52
|
+
get defaultSessionHttpOnly() { return defaultSessionHttpOnly }
|
|
53
|
+
|
|
54
|
+
get express() { return this.#express}
|
|
55
|
+
|
|
56
|
+
#__rootDirectory
|
|
57
|
+
get __rootDirectory() { return this.#__rootDirectory }
|
|
58
|
+
setRootDirectory(v) {
|
|
59
|
+
this.#__rootDirectory = v
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
context.setWebappsDirectory(path.join(__dirname, '..'))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
start(options) {
|
|
68
|
+
if (this.__rootDirectory == undefined) {
|
|
69
|
+
throw new Error('__rootDirectory belum didefinisikan')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
context.setRootDirectory(this.__rootDirectory)
|
|
73
|
+
context.setFnParseModuleRequest(options.fnParseModuleRequest)
|
|
74
|
+
|
|
75
|
+
if (this.#startedOnce) {
|
|
76
|
+
throw new Error('start already called!')
|
|
77
|
+
}
|
|
78
|
+
this.#startedOnce = true
|
|
79
|
+
|
|
80
|
+
// malai rutin utama untuk menjalankan server
|
|
81
|
+
main(this, options)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
export function createDefaultAppConfig() {
|
|
89
|
+
const sessionName = defaultSessionName
|
|
90
|
+
const sessionSecret = defaultSessionSecret
|
|
91
|
+
const sessionMaxAge = defaultSessionMaxAge
|
|
92
|
+
const sessionDomain = defaultSessionDomain
|
|
93
|
+
const sessionSecure = defaultSessionDomain
|
|
94
|
+
const sessionHttpOnly = defaultSessionHttpOnly
|
|
95
|
+
|
|
96
|
+
const notifierSocket = defaultNotifierSocket
|
|
97
|
+
const notifierServer = defaultNotifierServer
|
|
98
|
+
|
|
99
|
+
const redisUrl = defaultRedisUrl
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
sessionName,
|
|
103
|
+
sessionSecret,
|
|
104
|
+
sessionMaxAge,
|
|
105
|
+
sessionDomain,
|
|
106
|
+
sessionSecure,
|
|
107
|
+
sessionHttpOnly,
|
|
108
|
+
|
|
109
|
+
notifierSocket,
|
|
110
|
+
notifierServer,
|
|
111
|
+
redisUrl
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async function main(self, options) {
|
|
117
|
+
const __rootDirectory = self.__rootDirectory
|
|
118
|
+
|
|
119
|
+
const app = self.express
|
|
120
|
+
const port = options.port ?? self.defaultPort
|
|
121
|
+
const startingMessage = options.startingMessage ?? `Starting webserver on port \x1b[1;33m${port}\x1b[0m`
|
|
122
|
+
const appConfig = options.appConfig || createDefaultAppConfig()
|
|
123
|
+
|
|
124
|
+
const basicRouter = createBasicRouter()
|
|
125
|
+
const extendedRouter = options.router || ExpressServer.Router({ mergeParams: true });
|
|
126
|
+
const router = ExpressServer.Router({ mergeParams: true });
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
const redisUrl = appConfig.redisUrl
|
|
130
|
+
const notifierSocket = appConfig.notifierSocket
|
|
131
|
+
const notifierServer = appConfig.notifierServer
|
|
132
|
+
|
|
133
|
+
const sessionName = appConfig.sessionName
|
|
134
|
+
const sessionSecret = appConfig.sessionSecret
|
|
135
|
+
const sessionMaxAge = appConfig.sessionMaxAge
|
|
136
|
+
const sessionDomain = appConfig.sessionDomain
|
|
137
|
+
const sessionSecure = appConfig.sessionSecure
|
|
138
|
+
const sessionHttpOnly = appConfig.sessionHttpOnly
|
|
139
|
+
|
|
140
|
+
const session = await createSession({ redisUrl, sessionName, sessionSecret, sessionMaxAge, sessionDomain, sessionSecure, sessionHttpOnly })
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
router.use(uploader)
|
|
144
|
+
|
|
145
|
+
router.use(extendedRouter) // extended akan dipanggil dahulu, sehingga akan meng-override basicRouter
|
|
146
|
+
router.use(basicRouter)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// setup variabel konfigurasi local, nanti bisa diakses dari router/api
|
|
150
|
+
app.locals.appConfig = appConfig
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// konfigurasi applikasi
|
|
155
|
+
app.set('trust proxy', true); // ini nanti diisi daftar host yang dipercaya sebagai proxy, baca dari env
|
|
156
|
+
app.set("view engine", "ejs");
|
|
157
|
+
app.set("views", [
|
|
158
|
+
path.join(__rootDirectory, "views"),
|
|
159
|
+
path.join(__rootDirectory, 'public', 'modules')
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// setup cors
|
|
163
|
+
if (options.allowedOrigins!=null) {
|
|
164
|
+
const allowedOrigins = options.allowedOrigins
|
|
165
|
+
app.use(cors({
|
|
166
|
+
origin: function (origin, callback) {
|
|
167
|
+
if (!origin) return callback(null, true); // untuk server-side atau curl
|
|
168
|
+
const isAllowed = allowedOrigins.some(o => {
|
|
169
|
+
if (typeof o === 'string') return o === origin;
|
|
170
|
+
if (o instanceof RegExp) return o.test(origin);
|
|
171
|
+
return false;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (isAllowed) {
|
|
175
|
+
callback(null, true);
|
|
176
|
+
} else {
|
|
177
|
+
callback(new Error('Not allowed by CORS'));
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
// methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
181
|
+
// allowedHeaders: ['Content-Type', 'Authorization'],
|
|
182
|
+
// credentials: true
|
|
183
|
+
}));
|
|
184
|
+
} else {
|
|
185
|
+
app.use(cors());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
// setup middleware
|
|
191
|
+
app.use(favicon(path.join(__rootDirectory, 'public', 'favicon.ico')));
|
|
192
|
+
app.use(ExpressServer.json());
|
|
193
|
+
app.use(ExpressServer.urlencoded({ extended: true }));
|
|
194
|
+
app.use(session);
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// framework ini menggunakan library fgta5 untuk ui di client
|
|
198
|
+
if (appConfig.fgta5jsDebugMode) {
|
|
199
|
+
app.use('/public/libs/fgta5js', ExpressServer.static(path.join(__dirname, '..', 'libs', 'fgta5js')));
|
|
200
|
+
} else {
|
|
201
|
+
app.use('/public/libs/fgta5js', ExpressServer.static(path.join(__dirname, '..', 'libs', 'fgta5js-dist')));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
app.use('/public/libs/webmodule', ExpressServer.static(path.join(__dirname, '..', 'libs', 'webmodule')));
|
|
205
|
+
|
|
206
|
+
// Routing /public untuk serve halaman-halaman static
|
|
207
|
+
app.use('/public', rejectEjsFiles);
|
|
208
|
+
app.use('/public', ExpressServer.static(path.join(__rootDirectory, 'public')));
|
|
209
|
+
app.use('/', router)
|
|
210
|
+
app.use(handleModuleNotfound)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
const server = app.listen(port, ()=>{
|
|
214
|
+
console.log('\n\n' + startingMessage);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Tangani event 'error' pada objek server
|
|
218
|
+
server.on('error', (err) => {
|
|
219
|
+
if (err.code === 'EADDRINUSE') {
|
|
220
|
+
console.error(`\n\x1b[31mError!\x1b[0m\nPort ${port} sudah digunakan. Silakan coba port lain.\n`);
|
|
221
|
+
} else {
|
|
222
|
+
console.error('\n\x1b[31mError!\x1b[0m\nTerjadi kesalahan saat memulai server:', err, "\n");
|
|
223
|
+
}
|
|
224
|
+
// Anda bisa memutuskan untuk keluar dari aplikasi atau mencoba port lain
|
|
225
|
+
process.exit(1); // Keluar dengan kode error
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
function rejectEjsFiles(req, res, next) {
|
|
231
|
+
const excludedExtensions = ['.ejs'];
|
|
232
|
+
const ext = path.extname(req.url);
|
|
233
|
+
|
|
234
|
+
if (excludedExtensions.includes(ext)) {
|
|
235
|
+
return res.status(403).send('Akses ke file ini tidak diperbolehkan');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
next();
|
|
239
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Worker } from 'worker_threads';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const runDetachedWorker = (workerPath, notifierServer, clientId, options) => {
|
|
5
|
+
const timeout = options.timeout || 10000
|
|
6
|
+
|
|
7
|
+
// cek dulu apakah valid
|
|
8
|
+
try {
|
|
9
|
+
if (clientId==null || notifierServer==null) {
|
|
10
|
+
throw new Error('clientId / notifierServer belum didefinisikan di parameter runDetachedWorker')
|
|
11
|
+
}
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw err
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
console.log('Starting worker')
|
|
18
|
+
const worker = new Worker(workerPath, {
|
|
19
|
+
workerData: options
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const timeoutId = setTimeout(() => {
|
|
23
|
+
console.warn('Worker timeout, terminating...');
|
|
24
|
+
notifyClient(notifierServer, clientId, 'timeout') // nofify ke clent, kalau timeout
|
|
25
|
+
worker.terminate();
|
|
26
|
+
}, timeout);
|
|
27
|
+
|
|
28
|
+
worker.on('message', (info) => {
|
|
29
|
+
clearTimeout(timeoutId);
|
|
30
|
+
console.log('Worker message:', 'message', info);
|
|
31
|
+
if (info.done===true) {
|
|
32
|
+
notifyClient(notifierServer, clientId, 'done', info) // nofify message ke clent
|
|
33
|
+
// worker.terminate(); // Optional: let it exit naturally if preferred
|
|
34
|
+
} else {
|
|
35
|
+
notifyClient(notifierServer, clientId, 'message', info) // nofify message ke clent
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
worker.on('error', (err) => {
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
console.error('Worker error:', err);
|
|
42
|
+
notifyClient(notifierServer, clientId, 'error', {message: err.message}) // nofify error ke clent
|
|
43
|
+
worker.terminate();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
worker.on('exit', (code) => {
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
notifyClient(notifierServer, clientId, 'done', {})
|
|
49
|
+
console.log(`🚪 Worker exited with code ${code}`);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async function notifyClient(notifierServer, clientId, status, info) {
|
|
56
|
+
try {
|
|
57
|
+
console.log(status)
|
|
58
|
+
console.log('notify to:' , notifierServer)
|
|
59
|
+
console.log(clientId)
|
|
60
|
+
|
|
61
|
+
const data = {clientId, status, info}
|
|
62
|
+
const url = `${notifierServer}/notify`
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(data)
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const status = response.status
|
|
73
|
+
const statustext = response.statusText
|
|
74
|
+
const err = new Error(`${status} ${statustext}: ${options.method} ${url}`)
|
|
75
|
+
err.code = status
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return response
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw err
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
<!-- Component -->
|
|
3
|
+
<script type="module" src="public/libs/fgta5js/src/main.mjs"></script>
|
|
4
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-root.css" />
|
|
5
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-messagebox.css" />
|
|
6
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-modal.css" />
|
|
7
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-entry.css" />
|
|
8
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-checkbox.css" />
|
|
9
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-gridview.css" />
|
|
10
|
+
<link rel="stylesheet" href="public/libs/fgta5js/styles/fgta5js-appmanager.css" />
|
|
11
|
+
|