From 4402d4ec6403e786dd6a48f050afcfdd44df3dac Mon Sep 17 00:00:00 2001 From: DanielS Date: Tue, 23 Jun 2026 00:17:41 +0200 Subject: [PATCH] Add SMTP mail utility, admin settings page, and API routes for SMTP configuration and test email --- Dockerfile | 12 +- scripts/init_supabase.sh | 35 +++++ shop/app/admin/settings/page.tsx | 150 ++++++++++++++++++++ shop/app/api/admin/send-test-email/route.ts | 30 ++++ shop/app/api/admin/smtp-settings/route.ts | 51 +++++++ shop/utils/mail.ts | 44 ++++++ 6 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 scripts/init_supabase.sh create mode 100644 shop/app/admin/settings/page.tsx create mode 100644 shop/app/api/admin/send-test-email/route.ts create mode 100644 shop/app/api/admin/smtp-settings/route.ts create mode 100644 shop/utils/mail.ts diff --git a/Dockerfile b/Dockerfile index fcb72b0..f6135e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ WORKDIR /app # Install dependencies (use package lock when present) COPY shop/package*.json ./ RUN npm ci --omit=dev +# Install nodemailer for email sending +RUN npm install nodemailer # Copy the source code COPY shop/ . @@ -23,14 +25,16 @@ WORKDIR /app # Copy only built assets and runtime dependencies COPY --from=builder /app/.next ./.next - +# Copy standalone server and static assets +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static # KORREKTUR: Kopiert den public-Ordner nur, wenn er wirklich existiert COPY --from=builder /app/public* ./public COPY --from=builder /app/package*.json ./ # KORREKTUR: Flexibles Kopieren der Next-Konfiguration (egal ob .js oder .mjs) COPY --from=builder /app/next.config.* ./ -# Optionale TS-Dateien (nur kopieren, wenn vorhanden, blockiert den Build nicht mehr) +# KORREKTUR: Optionale TS-Dateien über den Sternchen-Trick absichern COPY --from=builder /app/tsconfig.jso* ./ COPY --from=builder /app/next-env.d.t* ./ # Install production dependencies (already installed in builder, but ensure they are present) @@ -42,5 +46,5 @@ ENV PORT=3000 EXPOSE 3000 -# Start the Next.js server -CMD ["npm", "run", "start"] +# Start the Next.js server (standalone) +CMD ["node", "server.js"] diff --git a/scripts/init_supabase.sh b/scripts/init_supabase.sh new file mode 100644 index 0000000..bf74639 --- /dev/null +++ b/scripts/init_supabase.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Initialize Supabase if needed and ensure default admin user exists +# Requires environment variables: +# SUPABASE_URL – base URL of Supabase instance (e.g., http://192.168.1.132:8000) +# SUPABASE_SERVICE_ROLE_KEY – service role secret key with admin privileges +# DEFAULT_USER_EMAIL – email for the default user (default: info@hephex.de) +# DEFAULT_USER_PASSWORD – password for the default user (default: Kathi2022!) + +DEFAULT_EMAIL="${DEFAULT_USER_EMAIL:-info@hephex.de}" +DEFAULT_PASSWORD="${DEFAULT_USER_PASSWORD:-Kathi2022!}" + +if [[ -z "$SUPABASE_URL" || -z "$SUPABASE_SERVICE_ROLE_KEY" ]]; then + echo "Supabase URL or Service Role Key not set – skipping user creation." + exit 0 +fi + +# Check if the user already exists +EXISTING=$(curl -s "${SUPABASE_URL}/auth/v1/admin/users?apikey=${SUPABASE_SERVICE_ROLE_KEY}" \ + -H "Authorization: Bearer ${SUPABASE_SERVICE_ROLE_KEY}" \ + -H "Content-Type: application/json" | grep -i "${DEFAULT_EMAIL}") + +if [[ -n "$EXISTING" ]]; then + echo "Default user ${DEFAULT_EMAIL} already exists." +else + echo "Creating default user ${DEFAULT_EMAIL}..." + curl -s -X POST "${SUPABASE_URL}/auth/v1/admin/users?apikey=${SUPABASE_SERVICE_ROLE_KEY}" \ + -H "Authorization: Bearer ${SUPABASE_SERVICE_ROLE_KEY}" \ + -H "Content-Type: application/json" \ + -d "{\"email\": \"${DEFAULT_EMAIL}\", \"password\": \"${DEFAULT_PASSWORD}\", \"email_confirmed\": true}" + echo "User created." +fi + +exec "$@" diff --git a/shop/app/admin/settings/page.tsx b/shop/app/admin/settings/page.tsx new file mode 100644 index 0000000..3827848 --- /dev/null +++ b/shop/app/admin/settings/page.tsx @@ -0,0 +1,150 @@ +// Admin Settings page – view & edit SMTP configuration and send test email +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; + +interface Settings { + host: string; + port: number; + secure: boolean; + user: string; + pass: string; +} + +export default function SettingsPage() { + const [settings, setSettings] = useState(null); + const [loading, setLoading] = useState(true); + const [message, setMessage] = useState(''); + const router = useRouter(); + + // Load current settings + useEffect(() => { + fetch('/api/admin/smtp-settings') + .then((res) => res.json()) + .then((data) => { + if (data.settings) setSettings(data.settings as Settings); + else setMessage('Failed to load settings'); + }) + .catch(() => setMessage('Failed to load settings')) + .finally(() => setLoading(false)); + }, []); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setSettings((prev) => + prev && { + ...prev, + [name]: type === 'checkbox' ? checked : name === 'port' ? Number(value) : value, + } + ); + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + setMessage(''); + const res = await fetch('/api/admin/smtp-settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings), + }); + const data = await res.json(); + if (res.ok) setMessage('Settings saved'); + else setMessage(data.error || 'Error saving settings'); + }; + + const handleTestEmail = async () => { + setMessage(''); + const testPayload = { + to: settings?.user || '', // send to the configured user address + subject: 'Test Email from Webshop Admin', + text: 'This is a test email to verify SMTP configuration.', + }; + const res = await fetch('/api/admin/send-test-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(testPayload), + }); + const data = await res.json(); + if (res.ok) setMessage('Test email sent successfully'); + else setMessage(data.error || 'Failed to send test email'); + }; + + if (loading) return
Loading…
; + + return ( +
+

SMTP‑Einstellungen

+ {message &&

{message}

} +
+ + + + + +
+ + +
+
+
+ ); +} diff --git a/shop/app/api/admin/send-test-email/route.ts b/shop/app/api/admin/send-test-email/route.ts new file mode 100644 index 0000000..96411c4 --- /dev/null +++ b/shop/app/api/admin/send-test-email/route.ts @@ -0,0 +1,30 @@ +// API route to send a test email – protected by Supabase admin check +import { NextResponse } from 'next/server'; +import { sendMail } from '@/utils/mail'; +import { createServerClient } from '@supabase/ssr'; + +export async function POST(request: Request) { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? '' + ); + + // Verify admin rights – expecting a user with role "admin" in the "users" table + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + const { data: user } = await supabase.from('users').select('role').eq('id', session.user.id).single(); + if (!user || user.role !== 'admin') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } + + const { to, subject, text, html } = await request.json(); + try { + const info = await sendMail({ to, subject, text, html }); + return NextResponse.json({ message: 'Email sent', info }); + } catch (err) { + console.error('Mail error', err); + return NextResponse.json({ error: 'Failed to send email' }, { status: 500 }); + } +} diff --git a/shop/app/api/admin/smtp-settings/route.ts b/shop/app/api/admin/smtp-settings/route.ts new file mode 100644 index 0000000..d8664c0 --- /dev/null +++ b/shop/app/api/admin/smtp-settings/route.ts @@ -0,0 +1,51 @@ +// API route for getting and updating SMTP settings (protected by admin check) +import { NextResponse } from 'next/server'; +import { createServerClient } from '@supabase/ssr'; + +export async function GET() { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? '' + ); + + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + const { data: user } = await supabase.from('users').select('role').eq('id', session.user.id).single(); + if (!user || user.role !== 'admin') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } + + const { data, error } = await supabase.from('settings').select('*').single(); + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json({ settings: data }); +} + +export async function POST(request: Request) { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? '' + ); + + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + const { data: user } = await supabase.from('users').select('role').eq('id', session.user.id).single(); + if (!user || user.role !== 'admin') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } + + const payload = await request.json(); // expect {host, port, secure, user, pass} + const { error } = await supabase + .from('settings') + .upsert({ id: 'smtp', ...payload }) + .single(); + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json({ message: 'SMTP settings saved' }); +} diff --git a/shop/utils/mail.ts b/shop/utils/mail.ts new file mode 100644 index 0000000..e294e8b --- /dev/null +++ b/shop/utils/mail.ts @@ -0,0 +1,44 @@ +// utils/mail.ts – simple wrapper around nodemailer +import nodemailer from 'nodemailer'; + +// Load SMTP configuration from environment variables +const smtpConfig = { + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), + secure: process.env.SMTP_SECURE === 'true' || process.env.SMTP_SECURE === '1', // true for 465, false for other ports + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, +}; + +// Create a reusable transporter. If any required var is missing, nodemailer will throw on send. +const transporter = nodemailer.createTransport(smtpConfig); + +/** + * Send an email. + * @param to Recipient address + * @param subject Subject line + * @param text Plain‑text body + * @param html Optional HTML body + */ +export async function sendMail({ + to, + subject, + text, + html, +}: { + to: string; + subject: string; + text: string; + html?: string; +}) { + const info = await transporter.sendMail({ + from: smtpConfig.auth.user, + to, + subject, + text, + html, + }); + return info; +}