new features
This commit is contained in:
@@ -1,49 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { pool } from '../db.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { requireAuth, requireSuperAdmin } from '../middleware/auth.js';
|
||||
|
||||
export const auditRouter = Router();
|
||||
auditRouter.use(requireAuth);
|
||||
auditRouter.use(requireSuperAdmin);
|
||||
|
||||
auditRouter.get('/', async (req, res) => {
|
||||
const user = req.user!;
|
||||
|
||||
if (user.role === 'super_admin') {
|
||||
// Super admin sees everything.
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 200`,
|
||||
);
|
||||
res.json(result.rows);
|
||||
return;
|
||||
}
|
||||
|
||||
// Domain admin: filter to entries that touch one of their domains.
|
||||
// We fetch a slightly larger window because filtering happens after the LIMIT
|
||||
// would skip relevant rows. 1000 should be plenty for a single domain.
|
||||
const allowed = (user.allowed_domains ?? []).map((d) => d.toLowerCase());
|
||||
if (allowed.length === 0) {
|
||||
res.json([]);
|
||||
return;
|
||||
}
|
||||
|
||||
auditRouter.get('/', async (_req, res) => {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 1000`,
|
||||
`SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 200`,
|
||||
);
|
||||
|
||||
const visible = result.rows
|
||||
.filter((row) => {
|
||||
const t = String(row.target_type ?? '').toLowerCase();
|
||||
const id = String(row.target_id ?? '').toLowerCase();
|
||||
if (t === 'domain') return allowed.includes(id);
|
||||
if (t === 'mailbox') {
|
||||
const at = id.lastIndexOf('@');
|
||||
if (at < 0) return false;
|
||||
return allowed.includes(id.slice(at + 1));
|
||||
}
|
||||
// 'node' and unknown types: globally scoped, hidden from domain admins.
|
||||
return false;
|
||||
})
|
||||
.slice(0, 200);
|
||||
|
||||
res.json(visible);
|
||||
res.json(result.rows);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,6 @@ async function refreshQuotaForDomain(req: any, domain: string): Promise<number>
|
||||
mailboxesRouter.get('/', async (req, res) => {
|
||||
const domain = String(req.query.domain ?? '').toLowerCase();
|
||||
const refreshQuota = String(req.query.refreshQuota ?? '').toLowerCase() === 'true';
|
||||
// Hide soft-deleted mailboxes by default. Set ?includeDeleted=true if you ever want them.
|
||||
const includeDeleted = String(req.query.includeDeleted ?? '').toLowerCase() === 'true';
|
||||
|
||||
if (domain) {
|
||||
@@ -126,6 +125,18 @@ mailboxesRouter.post('/:email/password', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Set storage quota for a single mailbox.
|
||||
// quota_gb is an integer between 1 and 1024 (GB).
|
||||
mailboxesRouter.post('/:email/quota', async (req, res) => {
|
||||
const body = z.object({ quota_gb: z.number().int().min(1).max(1024) }).parse(req.body);
|
||||
const email = normalizeEmail(req.params.email);
|
||||
ensureDomain(req, domainFromEmail(email));
|
||||
await dms.setQuota(email, body.quota_gb);
|
||||
await refreshQuotaForMailbox(email).catch((err) => console.warn(`Could not refresh quota for ${email}:`, err));
|
||||
await audit(req.user!.email, 'mailbox.quota_set', 'mailbox', email, { quota_gb: body.quota_gb }, req.ip);
|
||||
res.json({ ok: true, email, quota_gb: body.quota_gb });
|
||||
});
|
||||
|
||||
mailboxesRouter.get('/:email/rules', async (req, res) => {
|
||||
const email = normalizeEmail(req.params.email);
|
||||
ensureDomain(req, domainFromEmail(email));
|
||||
@@ -138,8 +149,6 @@ mailboxesRouter.put('/:email/rules', async (req, res) => {
|
||||
const body = z.object({
|
||||
ooo_active: z.boolean().optional(),
|
||||
ooo_message: z.string().optional(),
|
||||
// 'text' or 'html'. Stored as-is in DynamoDB so the email worker can
|
||||
// pick the correct Content-Type when sending the auto-reply.
|
||||
ooo_content_type: z.enum(['text', 'html']).optional(),
|
||||
forwards: z.array(z.string().email()).optional(),
|
||||
}).parse(req.body);
|
||||
@@ -167,4 +176,4 @@ mailboxesRouter.put('/:email/blocklist', async (req, res) => {
|
||||
const saved = await dynamo.putBlocklist(email, body.blocked_patterns);
|
||||
await audit(req.user!.email, 'mailbox.blocklist_update', 'mailbox', email, saved, req.ip);
|
||||
res.json(saved);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,6 +179,32 @@ export class DmsService {
|
||||
console.log(`[dms] password updated for mailbox: ${normalized}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the storage quota for a mailbox via the docker-mailserver
|
||||
* `setup quota set` subcommand. quotaGb must be a positive integer.
|
||||
* Equivalent to: docker exec mailserver setup quota set user@example.com 35G
|
||||
*/
|
||||
async setQuota(email: string, quotaGb: number): Promise<void> {
|
||||
const normalized = normalizeEmail(email);
|
||||
const gb = Math.floor(Number(quotaGb));
|
||||
|
||||
if (!Number.isFinite(gb) || gb <= 0 || gb > 1024) {
|
||||
throw new Error(`Invalid quota size: ${quotaGb}`);
|
||||
}
|
||||
|
||||
console.log(`[dms] setting quota for ${normalized} to ${gb}G`);
|
||||
|
||||
await this.assertDockerAccess();
|
||||
|
||||
await run(
|
||||
'docker',
|
||||
['exec', config.dmsContainer, 'setup', 'quota', 'set', normalized, `${gb}G`],
|
||||
120000,
|
||||
);
|
||||
|
||||
console.log(`[dms] quota set for ${normalized}: ${gb}G`);
|
||||
}
|
||||
|
||||
async syncSesDomain(domain: string): Promise<void> {
|
||||
const normalizedDomain = domain.toLowerCase();
|
||||
|
||||
@@ -204,4 +230,4 @@ export class DmsService {
|
||||
|
||||
return parseDoveadmQuota(stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user