feat: complete i18n support, telegram webhook, and security improvements
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
/**
|
||||
* lib/mail-session.ts
|
||||
* Stores/retrieves mail credentials in an encrypted httpOnly cookie.
|
||||
* Credentials never hit the database.
|
||||
*/
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
const COOKIE_NAME = "ayrismail_creds";
|
||||
|
||||
export interface MailSessionData {
|
||||
email: string;
|
||||
password: string;
|
||||
ownerId: string; // Dashboard user ID who owns this mail session
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,9 +26,19 @@ function decode(token: string): MailSessionData | null {
|
||||
}
|
||||
|
||||
/** Save mail credentials to cookie */
|
||||
export async function setMailSession(data: MailSessionData): Promise<void> {
|
||||
export async function setMailSession(data: Omit<MailSessionData, "ownerId">): Promise<void> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
throw new Error("Cannot set mail session without dashboard session");
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(COOKIE_NAME, encode(data), {
|
||||
const sessionData: MailSessionData = {
|
||||
...data,
|
||||
ownerId: session.user.id,
|
||||
};
|
||||
|
||||
cookieStore.set(COOKIE_NAME, encode(sessionData), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
@@ -43,10 +49,24 @@ export async function setMailSession(data: MailSessionData): Promise<void> {
|
||||
|
||||
/** Get mail credentials from cookie */
|
||||
export async function getMailSession(): Promise<MailSessionData | null> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const cookie = cookieStore.get(COOKIE_NAME);
|
||||
if (!cookie?.value) return null;
|
||||
return decode(cookie.value);
|
||||
|
||||
const data = decode(cookie.value);
|
||||
if (!data) return null;
|
||||
|
||||
// Verify that this mail session belongs to the current dashboard user
|
||||
if (data.ownerId !== session.user.id) {
|
||||
// Session belongs to another user, clear it for safety
|
||||
await clearMailSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Clear mail credentials cookie */
|
||||
|
||||
@@ -4,22 +4,38 @@
|
||||
* Uses the single super-admin API key from .env.
|
||||
*/
|
||||
|
||||
const BASE = process.env.MAILCOW_API_URL?.replace(/\/$/, "") ?? "";
|
||||
let BASE = process.env.MAILCOW_API_URL?.replace(/\/$/, "") ?? "";
|
||||
if (BASE && !BASE.startsWith("http")) {
|
||||
BASE = `https://${BASE}`;
|
||||
}
|
||||
const KEY = process.env.MAILCOW_API_KEY ?? "";
|
||||
|
||||
async function mfetch(path: string, options: RequestInit = {}) {
|
||||
if (!BASE || !KEY) {
|
||||
console.error("[Mailcow API] MAILCOW_API_URL or MAILCOW_API_KEY is not set in .env");
|
||||
return new Response(JSON.stringify({ error: "Server configuration error" }), { status: 500 });
|
||||
}
|
||||
const url = `${BASE}/api/v1${path}`;
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": KEY,
|
||||
...options.headers,
|
||||
},
|
||||
// Don't cache — always fresh
|
||||
cache: "no-store",
|
||||
});
|
||||
return res;
|
||||
console.log(`[Mailcow API] ${options.method || "GET"} ${url}`);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": KEY,
|
||||
...options.headers,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
console.error(`[Mailcow API] Error ${res.status}: ${text}`);
|
||||
}
|
||||
return res;
|
||||
} catch (err: any) {
|
||||
console.error(`[Mailcow API] Fetch failed: ${err.message}`);
|
||||
return new Response(JSON.stringify({ error: "Connection to Mailcow failed" }), { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────
|
||||
@@ -88,11 +104,14 @@ export async function createDomain(payload: {
|
||||
active: 1,
|
||||
aliases: 400,
|
||||
mailboxes: 10,
|
||||
defquota: 3072,
|
||||
maxquota: 10240,
|
||||
defquota: Math.min(payload.quota || 10240, 3072),
|
||||
maxquota: payload.quota || 10240,
|
||||
quota: 10240,
|
||||
...payload,
|
||||
};
|
||||
// Double check constraints
|
||||
if (body.maxquota > body.quota) body.maxquota = body.quota;
|
||||
if (body.defquota > body.maxquota) body.defquota = body.maxquota;
|
||||
const res = await mfetch("/add/domain", { method: "POST", body: JSON.stringify(body) });
|
||||
const data = await res.json();
|
||||
return { ok: res.ok, data };
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface AppUser {
|
||||
password: string; // plain text — store hashed in prod or use secrets manager
|
||||
role: "SUPER_ADMIN" | "DOMAIN_ADMIN";
|
||||
domains: string[]; // ["*"] for super admin, ["domain.com"] for domain admins
|
||||
telegramId?: string; // Optional Telegram ID for notifications
|
||||
}
|
||||
|
||||
/** Load all users defined in environment variables */
|
||||
@@ -36,6 +37,7 @@ export function getUsers(): AppUser[] {
|
||||
const password = process.env[`USER_${i}_PASSWORD`];
|
||||
const role = process.env[`USER_${i}_ROLE`] as AppUser["role"];
|
||||
const domainsRaw = process.env[`USER_${i}_DOMAINS`] ?? "";
|
||||
const telegramId = process.env[`USER_${i}_TELEGRAM_ID`];
|
||||
|
||||
if (!name || !email || !password) break;
|
||||
|
||||
@@ -46,6 +48,7 @@ export function getUsers(): AppUser[] {
|
||||
password,
|
||||
role: role ?? "DOMAIN_ADMIN",
|
||||
domains: domainsRaw === "*" ? ["*"] : domainsRaw.split(",").map((d) => d.trim()).filter(Boolean),
|
||||
telegramId,
|
||||
});
|
||||
|
||||
i++;
|
||||
|
||||
Reference in New Issue
Block a user