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)
|
# 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
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