Files
nixpanel-server/server/src/routes/nixserver/ssl.ts
T
NixReaper 8ff00c544c feat: v0.4.0 — Apache service stack, shell access, log viewer, changelog
Service stack (backend):
- webserver.ts rewritten: apache2/php-fpm routes replacing nginx; per-account
  Apache log endpoint; PHP-FPM reload per version; apache2ctl configtest/reload
- accounts.ts: added PUT /:id/shell to toggle /bin/bash vs /usr/sbin/nologin,
  runs usermod -s and writes audit log

New UI features (nixserver):
- ShellAccess page: grant/revoke SSH access per account via AccountPicker
- ApacheLogs page: live log viewer (access/error) per account; replaces stub
- NginxLogs: re-exports ApacheLogs (NixPanel uses Apache, not NGINX)
- WebServer page: shows apache2, php8.2-fpm, php8.3-fpm status with reload
  buttons; config test; Apache log viewer

Bug fixes:
- Accounts.tsx: Lucide icon size prop type widened (string|number); removed
  unused Users import; dropped duplicate NGINX log card
- Removed soon badge from ShellAccess and ApacheLogs entries

Project:
- CHANGELOG.md created with full history from 0.2.x through 0.4.0
- All packages bumped from 0.3.2 to 0.4.0
2026-04-11 05:17:49 -05:00

140 lines
4.9 KiB
TypeScript

import type { FastifyInstance } from 'fastify'
import { z } from 'zod'
import prisma from '../../db/client.js'
import { requireAdminOrReseller } from '../../middleware/auth.js'
import { exec } from '../../core/exec.js'
import { enableVhost } from '../../core/apache.js'
import { getCertPaths } from '../../core/ssl.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', '--apache',
'--non-interactive',
'--agree-tos',
'--email', account.email,
...domains.flatMap(d => ['-d', d]),
]
const result = await exec('certbot', certbotArgs, { timeout: 120_000 })
if (result.exitCode !== 0) {
return reply.code(500).send({ success: false, error: `certbot failed: ${result.stderr}` })
}
const { certPath, keyPath, chainPath } = getCertPaths(domain)
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 },
})
}
// Regenerate Apache vhost with SSL enabled
try {
await enableVhost({
domain,
username: account.username,
documentRoot: `${account.homedir}/public_html`,
sslEnabled: true,
certPath,
keyPath,
})
} catch (e) {
console.warn(`Apache SSL vhost 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' } })
})
}