fix(admin): persist data directory and pass env vars
This commit is contained in:
parent
99e8201745
commit
1d7e7c41c9
39 changed files with 1297 additions and 32 deletions
69
lib/admin/auth.ts
Normal file
69
lib/admin/auth.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { createHmac, timingSafeEqual } from "crypto";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const adminSessionCookie = "fs_admin_session";
|
||||
|
||||
const sessionMaxAgeSeconds = 60 * 60 * 8;
|
||||
|
||||
function secret() {
|
||||
return process.env.ADMIN_SESSION_SECRET ?? "dev-admin-session-secret-change-me";
|
||||
}
|
||||
|
||||
function adminEmail() {
|
||||
return process.env.ADMIN_EMAIL;
|
||||
}
|
||||
|
||||
function adminPassword() {
|
||||
return process.env.ADMIN_PASSWORD;
|
||||
}
|
||||
|
||||
function sign(value: string) {
|
||||
return createHmac("sha256", secret()).update(value).digest("base64url");
|
||||
}
|
||||
|
||||
function safeEqual(left: string, right: string) {
|
||||
const leftBuffer = Buffer.from(left);
|
||||
const rightBuffer = Buffer.from(right);
|
||||
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
||||
}
|
||||
|
||||
export function validateAdminCredentials(email: string, password: string) {
|
||||
const configuredEmail = adminEmail();
|
||||
const configuredPassword = adminPassword();
|
||||
if (!configuredEmail || !configuredPassword) return false;
|
||||
return safeEqual(email.trim().toLowerCase(), configuredEmail.toLowerCase()) && safeEqual(password, configuredPassword);
|
||||
}
|
||||
|
||||
export function createAdminSession() {
|
||||
const expiresAt = Date.now() + sessionMaxAgeSeconds * 1000;
|
||||
const payload = Buffer.from(JSON.stringify({ sub: adminEmail() ?? "admin", exp: expiresAt })).toString("base64url");
|
||||
return `${payload}.${sign(payload)}`;
|
||||
}
|
||||
|
||||
export function verifyAdminSession(token?: string) {
|
||||
if (!token) return false;
|
||||
const [payload, signature] = token.split(".");
|
||||
if (!payload || !signature || !safeEqual(signature, sign(payload))) return false;
|
||||
|
||||
try {
|
||||
const session = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { exp?: number };
|
||||
return typeof session.exp === "number" && session.exp > Date.now();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAdminSession() {
|
||||
const cookieStore = await cookies();
|
||||
return verifyAdminSession(cookieStore.get(adminSessionCookie)?.value);
|
||||
}
|
||||
|
||||
export function adminCookieOptions() {
|
||||
return {
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
secure: process.env.AUTH_COOKIE_SECURE === "true",
|
||||
path: "/",
|
||||
maxAge: sessionMaxAgeSeconds,
|
||||
};
|
||||
}
|
||||
139
lib/admin/store.ts
Normal file
139
lib/admin/store.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import type { ContactInquiry, InquiryStatus, RepairInquiry, SiteSettings, SmtpSettings } from "./types";
|
||||
|
||||
const dataDir = path.join(process.cwd(), "data");
|
||||
|
||||
async function ensureDataDir() {
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function readJson<T>(fileName: string, fallback: T): Promise<T> {
|
||||
await ensureDataDir();
|
||||
try {
|
||||
const file = await readFile(path.join(dataDir, fileName), "utf8");
|
||||
return JSON.parse(file) as T;
|
||||
} catch {
|
||||
await writeJson(fileName, fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeJson<T>(fileName: string, value: T) {
|
||||
await ensureDataDir();
|
||||
await writeFile(path.join(dataDir, fileName), `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function id(prefix: string) {
|
||||
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function text(value: FormDataEntryValue | null) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
export async function getContactInquiries() {
|
||||
return readJson<ContactInquiry[]>("contact-inquiries.json", []);
|
||||
}
|
||||
|
||||
export async function getRepairInquiries() {
|
||||
return readJson<RepairInquiry[]>("repair-inquiries.json", []);
|
||||
}
|
||||
|
||||
export async function addContactInquiry(form: FormData) {
|
||||
const inquiries = await getContactInquiries();
|
||||
const inquiry: ContactInquiry = {
|
||||
id: id("contact"),
|
||||
createdAt: new Date().toISOString(),
|
||||
status: "new",
|
||||
name: text(form.get("name")),
|
||||
email: text(form.get("email")),
|
||||
phone: text(form.get("phone")),
|
||||
subject: text(form.get("subject")) || "Kontaktanfrage",
|
||||
message: text(form.get("message")),
|
||||
};
|
||||
await writeJson("contact-inquiries.json", [inquiry, ...inquiries]);
|
||||
return inquiry;
|
||||
}
|
||||
|
||||
export async function addRepairInquiry(form: FormData) {
|
||||
const inquiries = await getRepairInquiries();
|
||||
const inquiry: RepairInquiry = {
|
||||
id: id("repair"),
|
||||
createdAt: new Date().toISOString(),
|
||||
status: "new",
|
||||
name: text(form.get("name")),
|
||||
email: text(form.get("email")),
|
||||
phone: text(form.get("phone")),
|
||||
manufacturer: text(form.get("manufacturer")),
|
||||
model: text(form.get("model")),
|
||||
serialNumber: text(form.get("serialNumber")),
|
||||
deviceType: text(form.get("deviceType")),
|
||||
accessories: text(form.get("accessories")),
|
||||
opened: text(form.get("opened")),
|
||||
previousWork: text(form.get("previousWork")),
|
||||
description: text(form.get("description")),
|
||||
};
|
||||
await writeJson("repair-inquiries.json", [inquiry, ...inquiries]);
|
||||
return inquiry;
|
||||
}
|
||||
|
||||
export async function updateContactStatus(id: string, status: InquiryStatus) {
|
||||
const inquiries = await getContactInquiries();
|
||||
await writeJson("contact-inquiries.json", inquiries.map((item) => item.id === id ? { ...item, status } : item));
|
||||
}
|
||||
|
||||
export async function updateRepairStatus(id: string, status: InquiryStatus) {
|
||||
const inquiries = await getRepairInquiries();
|
||||
await writeJson("repair-inquiries.json", inquiries.map((item) => item.id === id ? { ...item, status } : item));
|
||||
}
|
||||
|
||||
export async function getSiteSettings() {
|
||||
return readJson<SiteSettings>("site-settings.json", {
|
||||
companyName: "Funktechnik Schubert",
|
||||
phone: "",
|
||||
email: "info@funktechnik-schubert.de",
|
||||
address: "",
|
||||
openingHours: "",
|
||||
googleMapsEmbedUrl: "",
|
||||
socialLinks: "",
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveSiteSettings(form: FormData) {
|
||||
const settings: SiteSettings = {
|
||||
companyName: text(form.get("companyName")),
|
||||
phone: text(form.get("phone")),
|
||||
email: text(form.get("email")),
|
||||
address: text(form.get("address")),
|
||||
openingHours: text(form.get("openingHours")),
|
||||
googleMapsEmbedUrl: text(form.get("googleMapsEmbedUrl")),
|
||||
socialLinks: text(form.get("socialLinks")),
|
||||
};
|
||||
await writeJson("site-settings.json", settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function getSmtpSettings() {
|
||||
return readJson<SmtpSettings>("smtp-settings.json", {
|
||||
host: "",
|
||||
port: "587",
|
||||
username: "",
|
||||
fromAddress: "",
|
||||
replyToAddress: "",
|
||||
tls: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveSmtpSettings(form: FormData) {
|
||||
const settings: SmtpSettings = {
|
||||
host: text(form.get("host")),
|
||||
port: text(form.get("port")),
|
||||
username: text(form.get("username")),
|
||||
fromAddress: text(form.get("fromAddress")),
|
||||
replyToAddress: text(form.get("replyToAddress")),
|
||||
tls: form.get("tls") === "on",
|
||||
};
|
||||
await writeJson("smtp-settings.json", settings);
|
||||
return settings;
|
||||
}
|
||||
48
lib/admin/types.ts
Normal file
48
lib/admin/types.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
export type InquiryStatus = "new" | "in_progress" | "done";
|
||||
|
||||
export type ContactInquiry = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status: InquiryStatus;
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
subject?: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type RepairInquiry = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status: InquiryStatus;
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
serialNumber?: string;
|
||||
deviceType: string;
|
||||
accessories?: string;
|
||||
opened?: string;
|
||||
previousWork?: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type SiteSettings = {
|
||||
companyName: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
openingHours: string;
|
||||
googleMapsEmbedUrl: string;
|
||||
socialLinks: string;
|
||||
};
|
||||
|
||||
export type SmtpSettings = {
|
||||
host: string;
|
||||
port: string;
|
||||
username: string;
|
||||
fromAddress: string;
|
||||
replyToAddress: string;
|
||||
tls: boolean;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue