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
Some checks failed
Staging Build / build (push) Failing after 25s
This commit is contained in:
12
Dockerfile
12
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"]
|
||||
|
||||
35
scripts/init_supabase.sh
Normal file
35
scripts/init_supabase.sh
Normal 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 "$@"
|
||||
150
shop/app/admin/settings/page.tsx
Normal file
150
shop/app/admin/settings/page.tsx
Normal 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">SMTP‑Einstellungen</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 (SMTP‑User)
|
||||
<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"
|
||||
>
|
||||
Test‑Mail senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
shop/app/api/admin/send-test-email/route.ts
Normal file
30
shop/app/api/admin/send-test-email/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
51
shop/app/api/admin/smtp-settings/route.ts
Normal file
51
shop/app/api/admin/smtp-settings/route.ts
Normal 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
44
shop/utils/mail.ts
Normal 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 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;
|
||||
}
|
||||
Reference in New Issue
Block a user