#!/usr/bin/env node
import EventEmitter from 'events'
import { fileURLToPath } from 'url'
import fs from 'fs/promises'
import http from 'http'
import { randomBytes } from 'crypto'
import { spawn } from 'child_process'
import Utils from './Utils.js'
/**
* Builder for the web app
*/
export default class UiServer extends EventEmitter {
constructor (options) {
super()
this._handlers = { get: {}, post: {} }
this._server = http.createServer(this.requestHandler.bind(this))
this.fileRoot = fileURLToPath(new URL('../public', import.meta.url).href)
this.port = 8080
this.options = options
if (this.options.action === 'install') { // install-specific endpoints
this.get('/schemas/config', this.schemasHandler.bind(this))
this.get('/schemas/user', this.schemasHandler.bind(this))
this.post('/download', this.downloadHandler.bind(this))
this.post('/prereq', this.prereqHandler.bind(this))
this.get('/secrets', this.generateSecrets.bind(this))
this.post('/registeruser', this.registerUserHandler.bind(this))
this.post('/save', this.saveConfigHandler.bind(this))
this.post('/start', this.startAppHandler.bind(this))
}
if (this.options.devMode) {
this.get('/modules', this.getModulesHandler.bind(this))
this.post('/installmodules', this.installModulesHandler.bind(this))
}
if (this.options.action === 'update') { // update-specific endpoints
this.post('/update', this.updateHandler.bind(this))
}
this.get('/commands', this.commandHandler.bind(this))
this.get('/releases', this.getReleasesHandler.bind(this))
this.post('/exit', this.exitHandler.bind(this))
this._server.listen(this.port, () => {
console.log('Application running. \nIf the page doesn\'t open automatically, please visit http://localhost:8080 in your web browser.')
this.openBrowser()
})
}
get (route, handler) { this._handlers.get[route] = handler }
post (route, handler) { this._handlers.post[route] = handler }
openBrowser () {
const getCommand = (platform = process.platform) => {
if (platform === 'darwin') return 'open'
if (platform === 'win32') return 'start'
return 'xdg-open'
}
spawn(`${getCommand()} http://localhost:${this.port}`, { shell: true })
.on('error', e => console.log('spawn error', e))
}
async requestHandler (req, res) {
res.send = (...args) => this.send(res, ...args)
res.json = (...args) => this.sendJson(res, ...args)
const serveSuccess = await this.tryStaticServe(req, res)
if (serveSuccess) {
return
}
req.query = Utils.parseQuery(req)
req.body = await Utils.parseBody(req)
const handler = this._handlers[req.method.toLowerCase()][req.url]
if (handler) {
const h = handler(req, res)
return h?.catch(e => {
res.send(e.message, e.statusCode || 500)
this.onExit(e)
})
}
res.send(undefined, 404)
}
async tryStaticServe (req, res) {
try { // serve static file
const isIndex = req.url === '/'
if (isIndex) req.url = '/index.html'
let contents = (await fs.readFile(`${this.fileRoot}${req.url}`))
const replaceStr = this.options.action + (this.options.devMode ? '-dev' : '')
if (isIndex) contents = contents.toString().replace(/{{action}}/g, replaceStr)
res.send(contents)
return true
} catch (e) {} // probably not a static file, so just continue
}
send (res, data, statusCode = 200) {
res.writeHead(statusCode)
if (data !== undefined) res.write(data)
res.end()
}
sendJson (res, data, statusCode = 200) {
this.send(res, JSON.stringify(data), statusCode)
}
onExit (error) {
this.emit('exit', error)
this._server.close()
}
async schemasHandler (req, res) {
if (!this.schemas) {
this.schemas = await Utils.getSchemas(this.options.cwd)
}
res.json(this.schemas[req.url.replace('/schemas/', '')])
}
async registerUserHandler (req, res) {
try {
await Utils.registerSuperUser({ ...this.options, superEmail: req.body.email, superPassword: req.body.password })
res.send(undefined, 201)
} catch (e) {
const app = await Utils.startApp(this.options)
res.send(e.constructor.name === 'AdaptError' ? app.lang.translate('en', e) : e.message, e.statusCode)
}
}
async saveConfigHandler (req, res) {
await Utils.saveConfig(this.options.cwd, req.body)
res.json({ rootDir: this.options.cwd })
}
async downloadHandler (req, res) {
let tag = req.query.tag
if (!tag) {
const [latestRelease] = await Utils.getReleases(this.options)
if (!latestRelease) {
throw new Error('No releases found')
}
tag = latestRelease.name
}
await Utils.cloneRepo({ ...this.options, tag })
res.end()
}
async prereqHandler (req, res) {
await Utils.checkPrerequisites(this.options)
res.send()
}
async generateSecrets (req, res) {
const setSecretsRecursive = async (schema, data = {}) => {
await Promise.all(Object.entries(schema.properties).map(async ([k, v]) => {
if (v.type === 'object' && v.properties) {
data[k] = await setSecretsRecursive(v)
} else if (v?._adapt?.isSecret) {
data[k] = await randomBytes(32).toString('hex')
}
}))
if (Object.values(data).length && Object.values(data).some(Boolean)) return data
}
const { config: schemas } = await Utils.getSchemas(this.options.cwd)
const data = {}
await Promise.all(Object.entries(schemas).map(async ([id, { schema }]) => {
data[id] = await setSecretsRecursive(schema)
}))
return res.json(data)
}
async startAppHandler (req, res) {
const app = await Utils.startApp(this.options)
const ui = await app.waitForModule('ui')
ui.postBuildHook.tap(() => res.send())
}
async getReleasesHandler (req, res) {
res.json(this.options.releaseData)
}
async getModulesHandler (req, res) {
res.json((await Utils.getAppDependencies(this.options.cwd)).adapt)
}
async installModulesHandler (req, res) {
await Utils.installLocalModules({ ...this.options, modules: req.body })
res.send()
}
async updateHandler (req, res) {
await Utils.updateRepo({ ...this.options, tag: req.query.version })
res.end()
}
async commandHandler (req, res) {
res.json(Utils.getStartCommands(this.options.cwd))
}
async exitHandler (req, res) {
res.end()
this.onExit(typeof req.body === 'string' ? new Error(req.body) : undefined)
}
}