|
|
|
|
@@ -1,293 +0,0 @@
|
|
|
|
|
import { Router } from 'express'
|
|
|
|
|
import multer from 'multer'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import fs from 'node:fs'
|
|
|
|
|
import fsp from 'node:fs/promises'
|
|
|
|
|
import { fetchReleaseVersions } from '../../shared/mojang'
|
|
|
|
|
import { fileDir } from '../../shared/paths'
|
|
|
|
|
import {
|
|
|
|
|
createNewPack,
|
|
|
|
|
deletePacks,
|
|
|
|
|
listDashboardPacks,
|
|
|
|
|
loadAccounts,
|
|
|
|
|
loadPackDefinition,
|
|
|
|
|
loadRootManifest,
|
|
|
|
|
normalizePackDefinition,
|
|
|
|
|
savePackDefinition,
|
|
|
|
|
updatePack
|
|
|
|
|
} from '../../shared/store'
|
|
|
|
|
import { PackDefinition } from '../../shared/types'
|
|
|
|
|
import { requireAuth } from '../middleware/auth'
|
|
|
|
|
|
|
|
|
|
export const opRouter = Router()
|
|
|
|
|
const upload = multer({ storage: multer.memoryStorage() })
|
|
|
|
|
|
|
|
|
|
function pickFirstValue(value: unknown): string {
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return typeof value[0] === 'string' ? value[0] : ''
|
|
|
|
|
}
|
|
|
|
|
return typeof value === 'string' ? value : ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sanitizeUploadFileName(name: string): string {
|
|
|
|
|
return name.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeAssetPathForWeb(filePath: string): string {
|
|
|
|
|
return filePath.replace(/\\/g, '/')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveUploadedPackAsset(packKey: string, bucket: 'loaders' | 'resourcepacks' | 'shaderpacks', file: Express.Multer.File): Promise<string> {
|
|
|
|
|
const safeName = sanitizeUploadFileName(file.originalname)
|
|
|
|
|
const relativePath = path.join('uploads', packKey, bucket, `${Date.now()}-${safeName}`)
|
|
|
|
|
const absolutePath = path.join(fileDir, relativePath)
|
|
|
|
|
await fsp.mkdir(path.dirname(absolutePath), { recursive: true })
|
|
|
|
|
await fsp.writeFile(absolutePath, file.buffer)
|
|
|
|
|
return normalizeAssetPathForWeb(relativePath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function mutatePackDefinition(packKey: string, mutate: (pack: PackDefinition) => void): Promise<void> {
|
|
|
|
|
const current = await loadPackDefinition(packKey)
|
|
|
|
|
if (current == null) {
|
|
|
|
|
throw new Error('서버팩 JSON을 찾을 수 없습니다.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const next = normalizePackDefinition(current)
|
|
|
|
|
mutate(next)
|
|
|
|
|
await savePackDefinition(packKey, next)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeUploadedAsset(relativePath: string): Promise<void> {
|
|
|
|
|
const absolutePath = path.join(fileDir, relativePath)
|
|
|
|
|
if (fs.existsSync(absolutePath)) {
|
|
|
|
|
await fsp.unlink(absolutePath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 { password } = req.body as { password?: string }
|
|
|
|
|
const accounts = await loadAccounts()
|
|
|
|
|
const matched = accounts.find((entry) => 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 packs = await listDashboardPacks()
|
|
|
|
|
res.render('op/dashboard', {
|
|
|
|
|
userId: _req.session.userId,
|
|
|
|
|
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 currentDefinition = await loadPackDefinition(packKey)
|
|
|
|
|
if (currentDefinition == null) {
|
|
|
|
|
throw new Error('서버팩 JSON을 찾을 수 없습니다.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalized = normalizePackDefinition({
|
|
|
|
|
...currentDefinition,
|
|
|
|
|
mcVersion: pickFirstValue(req.body.mcVersion),
|
|
|
|
|
recommendedJdkVersion: Number(pickFirstValue(req.body.recommendedJdkVersion)),
|
|
|
|
|
loaderType: pickFirstValue(req.body.loaderType) as PackDefinition['loaderType'],
|
|
|
|
|
loaderVersion: pickFirstValue(req.body.loaderVersion),
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
opRouter.post('/op/dashboard/:packName/assets/loader', requireAuth, upload.single('asset'), async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const packKey = pickFirstValue(req.params.packName)
|
|
|
|
|
if (req.file == null) {
|
|
|
|
|
throw new Error('업로드된 로더 파일이 없습니다.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const relativePath = await saveUploadedPackAsset(packKey, 'loaders', req.file)
|
|
|
|
|
await mutatePackDefinition(packKey, (pack) => {
|
|
|
|
|
pack.loaderInstallerPath = relativePath
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
res.redirect(`/op/dashboard/${packKey}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
opRouter.post('/op/dashboard/:packName/assets/resource-pack', requireAuth, upload.single('asset'), async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const packKey = pickFirstValue(req.params.packName)
|
|
|
|
|
if (req.file == null) {
|
|
|
|
|
throw new Error('업로드된 리소스팩 파일이 없습니다.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const relativePath = await saveUploadedPackAsset(packKey, 'resourcepacks', req.file)
|
|
|
|
|
await mutatePackDefinition(packKey, (pack) => {
|
|
|
|
|
pack.resourcePackFiles = [...(pack.resourcePackFiles ?? []), relativePath]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
res.redirect(`/op/dashboard/${packKey}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
opRouter.post('/op/dashboard/:packName/assets/shader-pack', requireAuth, upload.single('asset'), async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const packKey = pickFirstValue(req.params.packName)
|
|
|
|
|
if (req.file == null) {
|
|
|
|
|
throw new Error('업로드된 쉐이더 파일이 없습니다.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const relativePath = await saveUploadedPackAsset(packKey, 'shaderpacks', req.file)
|
|
|
|
|
await mutatePackDefinition(packKey, (pack) => {
|
|
|
|
|
pack.shaderPackFiles = [...(pack.shaderPackFiles ?? []), relativePath]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
res.redirect(`/op/dashboard/${packKey}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
opRouter.post('/op/dashboard/:packName/assets/loader/remove', requireAuth, async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const packKey = pickFirstValue(req.params.packName)
|
|
|
|
|
const targetPath = pickFirstValue(req.body.assetPath)
|
|
|
|
|
await removeUploadedAsset(targetPath)
|
|
|
|
|
await mutatePackDefinition(packKey, (pack) => {
|
|
|
|
|
if (pack.loaderInstallerPath === targetPath) {
|
|
|
|
|
pack.loaderInstallerPath = ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
res.redirect(`/op/dashboard/${packKey}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
opRouter.post('/op/dashboard/:packName/assets/resource-pack/remove', requireAuth, async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const packKey = pickFirstValue(req.params.packName)
|
|
|
|
|
const targetPath = pickFirstValue(req.body.assetPath)
|
|
|
|
|
await removeUploadedAsset(targetPath)
|
|
|
|
|
await mutatePackDefinition(packKey, (pack) => {
|
|
|
|
|
pack.resourcePackFiles = (pack.resourcePackFiles ?? []).filter((entry) => entry !== targetPath)
|
|
|
|
|
})
|
|
|
|
|
res.redirect(`/op/dashboard/${packKey}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
opRouter.post('/op/dashboard/:packName/assets/shader-pack/remove', requireAuth, async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const packKey = pickFirstValue(req.params.packName)
|
|
|
|
|
const targetPath = pickFirstValue(req.body.assetPath)
|
|
|
|
|
await removeUploadedAsset(targetPath)
|
|
|
|
|
await mutatePackDefinition(packKey, (pack) => {
|
|
|
|
|
pack.shaderPackFiles = (pack.shaderPackFiles ?? []).filter((entry) => entry !== targetPath)
|
|
|
|
|
})
|
|
|
|
|
res.redirect(`/op/dashboard/${packKey}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error)
|
|
|
|
|
}
|
|
|
|
|
})
|