SendSecret: Erstimport und Umstellung auf Tag-Deployment
Wrapper-Frontend vor cryptgeon: der Browser verschluesselt lokal, per Mail geht nur der Link raus. Fuer das Deployment nach den Konventionen aus DEPLOY.md hergerichtet: - docker-compose.yml mit festem Projekt- und Container-Namen, kein ports-Mapping, Healthcheck als Deploy-Gate. cryptgeon und redis liegen im internen Netz, nur app haengt im Web-Netz. - cryptgeon von latest auf 2.9.3 gepinnt. Das ist derselbe Stand, den latest bisher geliefert hat; 2.6.2 existiert nicht. - /healthz in server.js, vor dem Catch-all-Proxy registriert. - Dockerfile auf npm ci mit Lockfile und non-root umgestellt. - .gitea/workflows/deploy.yml: Build und Syntaxpruefung vor dem Deploy, .env aus dem Repo-Secret DOTENV. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
110
app/server.js
Normal file
110
app/server.js
Normal file
@@ -0,0 +1,110 @@
|
||||
'use strict'
|
||||
|
||||
const express = require('express')
|
||||
const nodemailer = require('nodemailer')
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware')
|
||||
|
||||
const {
|
||||
SMTP_HOST,
|
||||
SMTP_PORT = '587',
|
||||
SMTP_USER,
|
||||
SMTP_PASS,
|
||||
SMTP_FROM,
|
||||
RECIPIENT_EMAIL,
|
||||
APP_URL = '',
|
||||
} = process.env
|
||||
|
||||
const app = express()
|
||||
// express.json() is intentionally NOT applied globally — it would consume the
|
||||
// request body stream before http-proxy-middleware can forward it to cryptgeon.
|
||||
app.use(express.static('public', { index: false }))
|
||||
|
||||
app.get('/', (_req, res) => res.sendFile('index.html', { root: 'public' }))
|
||||
app.get('/link', (_req, res) => res.sendFile('link.html', { root: 'public' }))
|
||||
|
||||
// Health-Endpoint fuer den Docker-Healthcheck. Muss vor dem Catch-all-Proxy
|
||||
// stehen, sonst landet die Anfrage bei cryptgeon.
|
||||
app.get('/healthz', (_req, res) => res.json({ ok: true }))
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: Number(SMTP_PORT),
|
||||
secure: Number(SMTP_PORT) === 465,
|
||||
auth: { user: SMTP_USER, pass: SMTP_PASS },
|
||||
})
|
||||
|
||||
app.post('/api/send', express.json(), async (req, res) => {
|
||||
const { url, name } = req.body
|
||||
|
||||
if (!url || typeof url !== 'string' || !url.includes('/note/')) {
|
||||
return res.status(400).json({ error: 'Ungültige Anfrage' })
|
||||
}
|
||||
if (!name || typeof name !== 'string' || !name.trim()) {
|
||||
return res.status(400).json({ error: 'Name fehlt' })
|
||||
}
|
||||
|
||||
const senderName = name.trim()
|
||||
|
||||
// Reject URLs that don't originate from our own domain
|
||||
if (APP_URL && !url.startsWith(APP_URL)) {
|
||||
return res.status(400).json({ error: 'Ungültige URL' })
|
||||
}
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: SMTP_FROM,
|
||||
to: RECIPIENT_EMAIL,
|
||||
subject: `Neues Passwort von ${senderName}`,
|
||||
text: [
|
||||
`${senderName} hat Ihnen sicher ein Passwort übermittelt.`,
|
||||
'',
|
||||
'Klicken Sie auf den folgenden Link um es einzusehen:',
|
||||
url,
|
||||
'',
|
||||
'Dieser Link ist einmalig abrufbar und löscht sich nach dem ersten Öffnen automatisch.',
|
||||
].join('\n'),
|
||||
html: `
|
||||
<div style="font-family:'Manrope',sans-serif;max-width:480px;margin:0 auto;padding:32px 24px;background:#fff;border:1px solid #E5E7EB;border-radius:4px;">
|
||||
<p style="margin:0 0 4px;font-size:12px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:#1E40AF;">
|
||||
Neues Passwort erhalten
|
||||
</p>
|
||||
<p style="margin:0 0 20px;font-size:16px;font-weight:600;color:#111827;">
|
||||
${senderName}
|
||||
</p>
|
||||
<p style="margin:0 0 20px;font-size:14px;color:#6B7280;line-height:1.6;">
|
||||
hat Ihnen sicher ein Passwort übermittelt.
|
||||
</p>
|
||||
<a href="${url}"
|
||||
style="display:inline-block;background:#1E40AF;color:#fff;padding:12px 20px;border-radius:4px;text-decoration:none;font-weight:600;font-size:14px;margin:0 0 24px;">
|
||||
Passwort abrufen →
|
||||
</a>
|
||||
<p style="margin:0;font-size:12px;color:#9CA3AF;line-height:1.5;border-top:1px solid #E5E7EB;padding-top:16px;">
|
||||
Dieser Link ist <strong>einmalig abrufbar</strong> und löscht sich nach dem ersten Öffnen automatisch.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
res.json({ ok: true })
|
||||
} catch (err) {
|
||||
console.error('Mail error:', err.message)
|
||||
res.status(500).json({ error: 'E-Mail konnte nicht gesendet werden' })
|
||||
}
|
||||
})
|
||||
|
||||
// All other requests → proxy to cryptgeon (API + note decrypt UI)
|
||||
app.use(
|
||||
createProxyMiddleware({
|
||||
target: 'http://cryptgeon:8000',
|
||||
changeOrigin: true,
|
||||
on: {
|
||||
error: (err, req, res) => {
|
||||
console.error(`Proxy error [${req.method} ${req.url}]:`, err.message)
|
||||
if (!res.headersSent) {
|
||||
res.status(502).json({ error: 'Cryptgeon nicht erreichbar' })
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
app.listen(3000, () => console.log('SendSecret listening on :3000'))
|
||||
Reference in New Issue
Block a user