feat: rename WHM→NixServer, cPanel→NixClient, bump v0.2.0
- Rename frontend workspaces: whm/ → nixserver/, cpanel/ → nixclient/ - Rename server route dirs: routes/whm/ → routes/nixserver/, routes/cpanel/ → routes/nixclient/ - Update all API prefixes: /api/whm → /api/nixserver, /api/cpanel → /api/nixclient - Rename env vars: WHM_PORT → NIXSERVER_PORT, CPANEL_PORT → NIXCLIENT_PORT - Update config properties: whmPort → nixserverPort, cpanelPort → nixclientPort - Update all UI labels, login pages, sidebar branding, and error messages - Update installer and update scripts to match new names - Add cpanel email route (missing from initial scaffold) - Add .gitattributes to enforce LF line endings - Bump version to 0.2.0 across all package.json files
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { z } from 'zod'
|
||||
import path from 'path'
|
||||
import prisma from '../../db/client.js'
|
||||
import { requireAdminOrReseller } from '../../middleware/auth.js'
|
||||
import { exec } from '../../core/exec.js'
|
||||
import { enableVhost } from '../../core/nginx.js'
|
||||
import { config } from '../../config.js'
|
||||
|
||||
export default async function sslRoutes(fastify: FastifyInstance) {
|
||||
const preHandler = [requireAdminOrReseller]
|
||||
|
||||
// GET /api/nixserver/ssl
|
||||
fastify.get('/', { preHandler }, async (request, reply) => {
|
||||
const q = request.query as Record<string, string>
|
||||
const where: Record<string, unknown> = {}
|
||||
if (q.accountId) where.accountId = parseInt(q.accountId, 10)
|
||||
if (q.status) where.status = q.status
|
||||
|
||||
const certs = await prisma.sslCertificate.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { account: { select: { username: true, domain: true } } },
|
||||
})
|
||||
return reply.send({ success: true, data: certs })
|
||||
})
|
||||
|
||||
// POST /api/nixserver/ssl/issue — issue Let's Encrypt cert via certbot
|
||||
fastify.post('/issue', { preHandler }, async (request, reply) => {
|
||||
const body = z.object({
|
||||
accountId: z.number().int().positive(),
|
||||
domain: z.string(),
|
||||
includeWww: z.boolean().default(true),
|
||||
autoRenew: z.boolean().default(true),
|
||||
}).safeParse(request.body)
|
||||
|
||||
if (!body.success) {
|
||||
return reply.code(400).send({ success: false, error: body.error.issues[0]?.message })
|
||||
}
|
||||
|
||||
const { accountId, domain, includeWww, autoRenew } = body.data
|
||||
|
||||
const account = await prisma.account.findUnique({ where: { id: accountId } })
|
||||
if (!account) return reply.code(404).send({ success: false, error: 'Account not found' })
|
||||
|
||||
const domains = [domain]
|
||||
if (includeWww) domains.push(`www.${domain}`)
|
||||
|
||||
const certbotArgs = [
|
||||
'certonly', '--webroot',
|
||||
'-w', config.paths.certbotWebroot,
|
||||
'--non-interactive',
|
||||
'--agree-tos',
|
||||
'--email', account.email,
|
||||
...domains.flatMap(d => ['-d', d]),
|
||||
]
|
||||
|
||||
const result = await exec('certbot', certbotArgs)
|
||||
if (result.exitCode !== 0) {
|
||||
return reply.code(500).send({ success: false, error: `certbot failed: ${result.stderr}` })
|
||||
}
|
||||
|
||||
const certDir = `/etc/letsencrypt/live/${domain}`
|
||||
const certPath = `${certDir}/fullchain.pem`
|
||||
const keyPath = `${certDir}/privkey.pem`
|
||||
const chainPath = `${certDir}/chain.pem`
|
||||
|
||||
const issuedAt = new Date()
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 90)
|
||||
|
||||
const existing = await prisma.sslCertificate.findFirst({ where: { accountId, domain } })
|
||||
let cert
|
||||
if (existing) {
|
||||
cert = await prisma.sslCertificate.update({
|
||||
where: { id: existing.id },
|
||||
data: { certPath, keyPath, chainPath, issuedAt, expiresAt, status: 'active', autoRenew },
|
||||
})
|
||||
} else {
|
||||
cert = await prisma.sslCertificate.create({
|
||||
data: { accountId, domain, certPath, keyPath, chainPath, issuedAt, expiresAt, status: 'active', autoRenew },
|
||||
})
|
||||
}
|
||||
|
||||
// Update nginx vhost with SSL
|
||||
try {
|
||||
await enableVhost({
|
||||
domain,
|
||||
username: account.username,
|
||||
documentRoot: `${account.homedir}/public_html`,
|
||||
sslEnabled: true,
|
||||
certPath,
|
||||
keyPath,
|
||||
chainPath,
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn(`Nginx SSL update failed for ${domain}:`, e)
|
||||
}
|
||||
|
||||
return reply.send({ success: true, data: cert })
|
||||
})
|
||||
|
||||
// POST /api/nixserver/ssl/revoke
|
||||
fastify.post('/:id/revoke', { preHandler }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const cert = await prisma.sslCertificate.findUnique({ where: { id: parseInt(id, 10) } })
|
||||
if (!cert) return reply.code(404).send({ success: false, error: 'Certificate not found' })
|
||||
|
||||
if (cert.certPath) {
|
||||
const result = await exec('certbot', ['revoke', '--cert-path', cert.certPath, '--non-interactive'])
|
||||
if (result.exitCode !== 0) {
|
||||
console.warn(`certbot revoke warning: ${result.stderr}`)
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.sslCertificate.update({
|
||||
where: { id: cert.id },
|
||||
data: { status: 'revoked' },
|
||||
})
|
||||
|
||||
return reply.send({ success: true, data: { message: 'Certificate revoked' } })
|
||||
})
|
||||
|
||||
// POST /api/nixserver/ssl/:id/renew
|
||||
fastify.post('/:id/renew', { preHandler }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const cert = await prisma.sslCertificate.findUnique({ where: { id: parseInt(id, 10) } })
|
||||
if (!cert) return reply.code(404).send({ success: false, error: 'Certificate not found' })
|
||||
|
||||
const result = await exec('certbot', ['renew', '--cert-name', cert.domain, '--non-interactive'])
|
||||
if (result.exitCode !== 0) {
|
||||
return reply.code(500).send({ success: false, error: `Renewal failed: ${result.stderr}` })
|
||||
}
|
||||
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 90)
|
||||
|
||||
await prisma.sslCertificate.update({
|
||||
where: { id: cert.id },
|
||||
data: { status: 'active', issuedAt: new Date(), expiresAt },
|
||||
})
|
||||
|
||||
return reply.send({ success: true, data: { message: 'Certificate renewed' } })
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user