Build installer and management site from spec
This commit is contained in:
58
src/server/app.ts
Normal file
58
src/server/app.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import express from 'express'
|
||||
import session from 'express-session'
|
||||
import path from 'node:path'
|
||||
import { fileDir, manifestDir, publicDir, viewsDir } from '../shared/paths'
|
||||
import { ensureProjectFiles } from '../shared/store'
|
||||
import { indexRouter } from './routes/index'
|
||||
import { opRouter } from './routes/op'
|
||||
|
||||
export async function createApp() {
|
||||
await ensureProjectFiles()
|
||||
|
||||
const app = express()
|
||||
app.set('view engine', 'ejs')
|
||||
app.set('views', viewsDir)
|
||||
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
app.use(express.json())
|
||||
app.use(
|
||||
session({
|
||||
secret: 'mc-custom-suite-session-secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.use('/static', express.static(publicDir))
|
||||
app.use('/manifest', express.static(manifestDir))
|
||||
app.use('/file', express.static(fileDir))
|
||||
|
||||
app.use(indexRouter)
|
||||
app.use(opRouter)
|
||||
|
||||
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
console.error(error)
|
||||
res.status(500).send('서버 내부 오류가 발생했습니다.')
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await createApp()
|
||||
const port = Number(process.env.PORT ?? 3000)
|
||||
app.listen(port, '127.0.0.1', () => {
|
||||
console.log(`Management site listening on http://127.0.0.1:${port}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
bootstrap().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
15
src/server/middleware/auth.ts
Normal file
15
src/server/middleware/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
|
||||
if (req.session.userId == null) {
|
||||
res.redirect('/op')
|
||||
return
|
||||
}
|
||||
next()
|
||||
}
|
||||
31
src/server/routes/index.ts
Normal file
31
src/server/routes/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Router } from 'express'
|
||||
import path from 'node:path'
|
||||
import { manifestRootPath } from '../../shared/paths'
|
||||
import { fetchReleaseVersions } from '../../shared/mojang'
|
||||
import { loadRootManifest } from '../../shared/store'
|
||||
|
||||
export const indexRouter = Router()
|
||||
|
||||
indexRouter.get('/', async (_req, res, next) => {
|
||||
try {
|
||||
const manifest = await loadRootManifest()
|
||||
res.render('index', {
|
||||
packs: manifest.packs
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
indexRouter.get('/manifest.json', (_req, res) => {
|
||||
res.sendFile(path.resolve(manifestRootPath))
|
||||
})
|
||||
|
||||
indexRouter.get('/api/releases', async (_req, res, next) => {
|
||||
try {
|
||||
const releases = await fetchReleaseVersions()
|
||||
res.json(releases)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
143
src/server/routes/op.ts
Normal file
143
src/server/routes/op.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Router } from 'express'
|
||||
import { fetchReleaseVersions } from '../../shared/mojang'
|
||||
import {
|
||||
createNewPack,
|
||||
deletePacks,
|
||||
loadAccounts,
|
||||
loadPackDefinition,
|
||||
loadRootManifest,
|
||||
normalizePackDefinition,
|
||||
updatePack
|
||||
} from '../../shared/store'
|
||||
import { requireAuth } from '../middleware/auth'
|
||||
|
||||
export const opRouter = Router()
|
||||
|
||||
function pickFirstValue(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return typeof value[0] === 'string' ? value[0] : ''
|
||||
}
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
opRouter.get('/op', (req, res) => {
|
||||
if (req.session.userId != null) {
|
||||
res.redirect('/op/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
res.render('op/login', {
|
||||
errorMessage: null
|
||||
})
|
||||
})
|
||||
|
||||
opRouter.post('/op/login', async (req, res, next) => {
|
||||
try {
|
||||
const { id, password } = req.body as { id?: string; password?: string }
|
||||
const accounts = await loadAccounts()
|
||||
const matched = accounts.find((entry) => entry.id === id && entry.password === password)
|
||||
|
||||
if (matched == null) {
|
||||
res.status(401).render('op/login', {
|
||||
errorMessage: '아이디 또는 비밀번호가 올바르지 않습니다.'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
req.session.userId = matched.id
|
||||
res.redirect('/op/dashboard')
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
opRouter.post('/op/logout', requireAuth, (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.redirect('/op')
|
||||
})
|
||||
})
|
||||
|
||||
opRouter.get('/op/dashboard', requireAuth, async (_req, res, next) => {
|
||||
try {
|
||||
const manifest = await loadRootManifest()
|
||||
res.render('op/dashboard', {
|
||||
userId: _req.session.userId,
|
||||
packs: manifest.packs
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
opRouter.post('/op/dashboard/packs', requireAuth, async (_req, res, next) => {
|
||||
try {
|
||||
const packKey = await createNewPack()
|
||||
res.redirect(`/op/dashboard/${packKey}`)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
opRouter.post('/op/dashboard/packs/delete', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const rawSelection = req.body.packKeys
|
||||
const packKeys = Array.isArray(rawSelection)
|
||||
? rawSelection.map(String)
|
||||
: typeof rawSelection === 'string'
|
||||
? [rawSelection]
|
||||
: []
|
||||
|
||||
await deletePacks(packKeys)
|
||||
res.redirect('/op/dashboard')
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
opRouter.get('/op/dashboard/:packName', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const packName = pickFirstValue(req.params.packName)
|
||||
const definition = await loadPackDefinition(packName)
|
||||
if (definition == null) {
|
||||
res.status(404).send('서버팩 JSON을 찾을 수 없습니다.')
|
||||
return
|
||||
}
|
||||
|
||||
const rootManifest = await loadRootManifest()
|
||||
const packEntry = rootManifest.packs.find((entry) => entry.file === packName)
|
||||
const releases = await fetchReleaseVersions()
|
||||
|
||||
res.render('op/editor', {
|
||||
userId: req.session.userId,
|
||||
packKey: packName,
|
||||
packEntry,
|
||||
pack: definition,
|
||||
releases
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const packKey = pickFirstValue(req.params.packName)
|
||||
const nextPackName = pickFirstValue(req.body.displayName).trim() || packKey
|
||||
const nextJsonKey = pickFirstValue(req.body.fileName).trim() || packKey
|
||||
|
||||
const normalized = normalizePackDefinition({
|
||||
mcVersion: pickFirstValue(req.body.mcVersion),
|
||||
serverMinRam: Number(pickFirstValue(req.body.serverMinRam)),
|
||||
serverMaxRam: Number(pickFirstValue(req.body.serverMaxRam)),
|
||||
clientMinRam: Number(pickFirstValue(req.body.clientMinRam)),
|
||||
clientRecommendedRam: Number(pickFirstValue(req.body.clientRecommendedRam)),
|
||||
packPath: pickFirstValue(req.body.packPath),
|
||||
description: pickFirstValue(req.body.description)
|
||||
})
|
||||
|
||||
const changedKey = await updatePack(packKey, nextPackName, nextJsonKey, normalized)
|
||||
res.redirect(`/op/dashboard/${changedKey}`)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user