Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
173
src/lib/services/email-accounts.service.ts
Normal file
173
src/lib/services/email-accounts.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { emailAccounts } from '@/lib/db/schema/email';
|
||||
import { encrypt, decrypt } from '@/lib/utils/encryption';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { NotFoundError, ForbiddenError } from '@/lib/errors';
|
||||
import type { ConnectAccountInput, ToggleAccountInput } from '@/lib/validators/email';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface AuditMeta {
|
||||
userId: string;
|
||||
portId: string;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
type AccountWithoutCredentials = Omit<typeof emailAccounts.$inferSelect, 'credentialsEnc'>;
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function stripCredentials(
|
||||
account: typeof emailAccounts.$inferSelect,
|
||||
): AccountWithoutCredentials {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { credentialsEnc: _, ...safe } = account;
|
||||
return safe;
|
||||
}
|
||||
|
||||
// ─── List ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listAccounts(
|
||||
userId: string,
|
||||
portId: string,
|
||||
): Promise<AccountWithoutCredentials[]> {
|
||||
const accounts = await db
|
||||
.select()
|
||||
.from(emailAccounts)
|
||||
.where(and(eq(emailAccounts.userId, userId), eq(emailAccounts.portId, portId)));
|
||||
|
||||
return accounts.map(stripCredentials);
|
||||
}
|
||||
|
||||
// ─── Connect ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function connectAccount(
|
||||
userId: string,
|
||||
portId: string,
|
||||
data: ConnectAccountInput,
|
||||
audit: AuditMeta,
|
||||
): Promise<AccountWithoutCredentials> {
|
||||
const credentialsEnc = encrypt(
|
||||
JSON.stringify({ username: data.username, password: data.password }),
|
||||
);
|
||||
|
||||
const inserted = await db
|
||||
.insert(emailAccounts)
|
||||
.values({
|
||||
userId,
|
||||
portId,
|
||||
provider: data.provider,
|
||||
emailAddress: data.emailAddress,
|
||||
smtpHost: data.smtpHost,
|
||||
smtpPort: data.smtpPort,
|
||||
imapHost: data.imapHost,
|
||||
imapPort: data.imapPort,
|
||||
credentialsEnc,
|
||||
isActive: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const account = inserted[0];
|
||||
if (!account) throw new Error('Failed to insert email account');
|
||||
|
||||
void createAuditLog({
|
||||
userId: audit.userId,
|
||||
portId: audit.portId,
|
||||
action: 'create',
|
||||
entityType: 'email_account',
|
||||
entityId: account.id,
|
||||
metadata: { emailAddress: data.emailAddress, provider: data.provider },
|
||||
ipAddress: audit.ipAddress,
|
||||
userAgent: audit.userAgent,
|
||||
});
|
||||
|
||||
return stripCredentials(account);
|
||||
}
|
||||
|
||||
// ─── Toggle ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function toggleAccount(
|
||||
accountId: string,
|
||||
userId: string,
|
||||
data: ToggleAccountInput,
|
||||
): Promise<AccountWithoutCredentials> {
|
||||
const existing = await db.query.emailAccounts.findFirst({
|
||||
where: eq(emailAccounts.id, accountId),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Email account');
|
||||
}
|
||||
|
||||
if (existing.userId !== userId) {
|
||||
throw new ForbiddenError('You do not own this email account');
|
||||
}
|
||||
|
||||
const updatedRows = await db
|
||||
.update(emailAccounts)
|
||||
.set({ isActive: data.isActive, updatedAt: new Date() })
|
||||
.where(eq(emailAccounts.id, accountId))
|
||||
.returning();
|
||||
|
||||
const updated = updatedRows[0];
|
||||
if (!updated) throw new Error('Failed to update email account');
|
||||
|
||||
return stripCredentials(updated);
|
||||
}
|
||||
|
||||
// ─── Disconnect ───────────────────────────────────────────────────────────────
|
||||
|
||||
export async function disconnectAccount(
|
||||
accountId: string,
|
||||
userId: string,
|
||||
audit: AuditMeta,
|
||||
): Promise<void> {
|
||||
const existing = await db.query.emailAccounts.findFirst({
|
||||
where: eq(emailAccounts.id, accountId),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Email account');
|
||||
}
|
||||
|
||||
if (existing.userId !== userId) {
|
||||
throw new ForbiddenError('You do not own this email account');
|
||||
}
|
||||
|
||||
await db.delete(emailAccounts).where(eq(emailAccounts.id, accountId));
|
||||
|
||||
void createAuditLog({
|
||||
userId: audit.userId,
|
||||
portId: audit.portId,
|
||||
action: 'delete',
|
||||
entityType: 'email_account',
|
||||
entityId: accountId,
|
||||
metadata: { emailAddress: existing.emailAddress },
|
||||
ipAddress: audit.ipAddress,
|
||||
userAgent: audit.userAgent,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Get Decrypted Credentials (INTERNAL ONLY) ────────────────────────────────
|
||||
|
||||
export async function getDecryptedCredentials(
|
||||
accountId: string,
|
||||
): Promise<{ username: string; password: string }> {
|
||||
const account = await db.query.emailAccounts.findFirst({
|
||||
where: eq(emailAccounts.id, accountId),
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new NotFoundError('Email account');
|
||||
}
|
||||
|
||||
const { username, password } = JSON.parse(decrypt(account.credentialsEnc)) as {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
return { username, password };
|
||||
}
|
||||
Reference in New Issue
Block a user