Add SMTP mail utility, admin settings page, and API routes for SMTP configuration and test email
Some checks failed
Staging Build / build (push) Failing after 25s

This commit is contained in:
DanielS
2026-06-23 00:17:41 +02:00
parent 4b9c289ec4
commit 4402d4ec64
6 changed files with 318 additions and 4 deletions

View File

@@ -9,6 +9,8 @@ WORKDIR /app
# Install dependencies (use package lock when present) # Install dependencies (use package lock when present)
COPY shop/package*.json ./ COPY shop/package*.json ./
RUN npm ci --omit=dev RUN npm ci --omit=dev
# Install nodemailer for email sending
RUN npm install nodemailer
# Copy the source code # Copy the source code
COPY shop/ . COPY shop/ .
@@ -23,14 +25,16 @@ WORKDIR /app
# Copy only built assets and runtime dependencies # Copy only built assets and runtime dependencies
COPY --from=builder /app/.next ./.next 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 # KORREKTUR: Kopiert den public-Ordner nur, wenn er wirklich existiert
COPY --from=builder /app/public* ./public COPY --from=builder /app/public* ./public
COPY --from=builder /app/package*.json ./ COPY --from=builder /app/package*.json ./
# KORREKTUR: Flexibles Kopieren der Next-Konfiguration (egal ob .js oder .mjs) # KORREKTUR: Flexibles Kopieren der Next-Konfiguration (egal ob .js oder .mjs)
COPY --from=builder /app/next.config.* ./ 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/tsconfig.jso* ./
COPY --from=builder /app/next-env.d.t* ./ COPY --from=builder /app/next-env.d.t* ./
# Install production dependencies (already installed in builder, but ensure they are present) # Install production dependencies (already installed in builder, but ensure they are present)
@@ -42,5 +46,5 @@ ENV PORT=3000
EXPOSE 3000 EXPOSE 3000
# Start the Next.js server # Start the Next.js server (standalone)
CMD ["npm", "run", "start"] CMD ["node", "server.js"]

35
scripts/init_supabase.sh Normal file
View File

@@ -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 "$@"

View File

@@ -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<Settings | null>(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<HTMLInputElement>) => {
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 <div className="p-8">Loading</div>;
return (
<div className="max-w-2xl mx-auto p-8 bg-black/30 backdrop-blur-xl rounded-lg text-white">
<h1 className="text-2xl font-bold mb-6">SMTPEinstellungen</h1>
{message && <p className="mb-4 text-yellow-300">{message}</p>}
<form onSubmit={handleSave} className="space-y-4">
<label className="flex flex-col">
Host
<input
name="host"
type="text"
required
value={settings?.host ?? ''}
onChange={handleChange}
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
/>
</label>
<label className="flex flex-col">
Port
<input
name="port"
type="number"
required
value={settings?.port ?? ''}
onChange={handleChange}
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
/>
</label>
<label className="inline-flex items-center space-x-2">
<input
name="secure"
type="checkbox"
checked={settings?.secure ?? false}
onChange={handleChange}
className="rounded"
/>
<span>SSL / TLS (secure)</span>
</label>
<label className="flex flex-col">
Benutzer (SMTPUser)
<input
name="user"
type="email"
required
value={settings?.user ?? ''}
onChange={handleChange}
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
/>
</label>
<label className="flex flex-col">
Passwort
<input
name="pass"
type="password"
required
value={settings?.pass ?? ''}
onChange={handleChange}
className="mt-1 p-2 rounded bg-black/20 border border-white/20"
/>
</label>
<div className="flex space-x-4 mt-4">
<button
type="submit"
className="px-4 py-2 bg-primary rounded hover:bg-primary/80 transition"
>
Speichern
</button>
<button
type="button"
onClick={handleTestEmail}
className="px-4 py-2 bg-green-600 rounded hover:bg-green-500 transition"
>
TestMail senden
</button>
</div>
</form>
</div>
);
}

View File

@@ -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 });
}
}

View File

@@ -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' });
}

44
shop/utils/mail.ts Normal file
View File

@@ -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 Plaintext 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;
}