feat: Add email signature feature

- Add email_signatures table to schema
- Add email signature service, controller, routes
- Users can create/edit signature in Profile
- Toggle to include signature when sending email replies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-17 19:11:51 +01:00
parent 514b6c8a92
commit 0523087961
5 changed files with 292 additions and 0 deletions

View File

@@ -30,6 +30,7 @@ import eventRoutes from './routes/event.routes.js';
import messageRoutes from './routes/message.routes.js'; import messageRoutes from './routes/message.routes.js';
import userRoutes from './routes/user.routes.js'; import userRoutes from './routes/user.routes.js';
import serviceRoutes from './routes/service.routes.js'; import serviceRoutes from './routes/service.routes.js';
import emailSignatureRoutes from './routes/email-signature.routes.js';
const app = express(); const app = express();
@@ -126,6 +127,7 @@ app.use('/api/events', eventRoutes);
app.use('/api/messages', messageRoutes); app.use('/api/messages', messageRoutes);
app.use('/api/users', userRoutes); app.use('/api/users', userRoutes);
app.use('/api/services', serviceRoutes); app.use('/api/services', serviceRoutes);
app.use('/api/email-signature', emailSignatureRoutes);
// Basic route // Basic route
app.get('/', (req, res) => { app.get('/', (req, res) => {

View File

@@ -0,0 +1,93 @@
import * as emailSignatureService from '../services/email-signature.service.js';
/**
* Get current user's email signature
* GET /api/email-signature
*/
export const getSignature = async (req, res, next) => {
try {
const signature = await emailSignatureService.getSignature(req.userId);
res.status(200).json({
success: true,
data: signature,
});
} catch (error) {
next(error);
}
};
/**
* Create or update email signature
* POST /api/email-signature
*/
export const upsertSignature = async (req, res, next) => {
try {
const signature = await emailSignatureService.upsertSignature(req.userId, req.body);
res.status(200).json({
success: true,
data: signature,
message: 'Podpis bol uložený',
});
} catch (error) {
next(error);
}
};
/**
* Toggle signature on/off
* PATCH /api/email-signature/toggle
*/
export const toggleSignature = async (req, res, next) => {
try {
const { isEnabled } = req.body;
const signature = await emailSignatureService.toggleSignature(req.userId, isEnabled);
res.status(200).json({
success: true,
data: signature,
message: isEnabled ? 'Podpis zapnutý' : 'Podpis vypnutý',
});
} catch (error) {
next(error);
}
};
/**
* Get formatted signature text for email
* GET /api/email-signature/formatted
*/
export const getFormattedSignature = async (req, res, next) => {
try {
const signature = await emailSignatureService.getSignature(req.userId);
const formatted = emailSignatureService.formatSignatureText(signature);
res.status(200).json({
success: true,
data: {
text: formatted,
isEnabled: signature?.isEnabled || false,
},
});
} catch (error) {
next(error);
}
};
/**
* Delete email signature
* DELETE /api/email-signature
*/
export const deleteSignature = async (req, res, next) => {
try {
const result = await emailSignatureService.deleteSignature(req.userId);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
next(error);
}
};

View File

@@ -310,6 +310,21 @@ export const services = pgTable('services', {
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}); });
// Email Signatures table - email podpisy používateľov
export const emailSignatures = pgTable('email_signatures', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull().unique(),
fullName: text('full_name'),
position: text('position'),
phone: text('phone'),
email: text('email'),
companyName: text('company_name'),
website: text('website'),
isEnabled: boolean('is_enabled').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Messages table - interná komunikácia medzi používateľmi // Messages table - interná komunikácia medzi používateľmi
export const messages = pgTable('messages', { export const messages = pgTable('messages', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),

View File

@@ -0,0 +1,42 @@
import express from 'express';
import * as emailSignatureController from '../controllers/email-signature.controller.js';
import { authenticate } from '../middlewares/auth/authMiddleware.js';
import { validateBody } from '../middlewares/security/validateInput.js';
import { z } from 'zod';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// Validation schemas
const signatureSchema = z.object({
fullName: z.string().max(255).optional().or(z.literal('')).or(z.null()),
position: z.string().max(255).optional().or(z.literal('')).or(z.null()),
phone: z.string().max(50).optional().or(z.literal('')).or(z.null()),
email: z.string().email('Neplatný formát emailu').max(255).optional().or(z.literal('')).or(z.null()),
companyName: z.string().max(255).optional().or(z.literal('')).or(z.null()),
website: z.string().max(255).optional().or(z.literal('')).or(z.null()),
isEnabled: z.boolean().optional(),
});
const toggleSchema = z.object({
isEnabled: z.boolean(),
});
// Get user's signature
router.get('/', emailSignatureController.getSignature);
// Get formatted signature text
router.get('/formatted', emailSignatureController.getFormattedSignature);
// Create or update signature
router.post('/', validateBody(signatureSchema), emailSignatureController.upsertSignature);
// Toggle signature on/off
router.patch('/toggle', validateBody(toggleSchema), emailSignatureController.toggleSignature);
// Delete signature
router.delete('/', emailSignatureController.deleteSignature);
export default router;

View File

@@ -0,0 +1,140 @@
import { db } from '../config/database.js';
import { emailSignatures } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
/**
* Get signature for a user
*/
export const getSignature = async (userId) => {
const [signature] = await db
.select()
.from(emailSignatures)
.where(eq(emailSignatures.userId, userId))
.limit(1);
return signature || null;
};
/**
* Create or update signature for a user
*/
export const upsertSignature = async (userId, data) => {
const existing = await getSignature(userId);
if (existing) {
// Update
const [updated] = await db
.update(emailSignatures)
.set({
fullName: data.fullName || null,
position: data.position || null,
phone: data.phone || null,
email: data.email || null,
companyName: data.companyName || null,
website: data.website || null,
isEnabled: data.isEnabled !== undefined ? data.isEnabled : existing.isEnabled,
updatedAt: new Date(),
})
.where(eq(emailSignatures.userId, userId))
.returning();
return updated;
} else {
// Create
const [created] = await db
.insert(emailSignatures)
.values({
userId,
fullName: data.fullName || null,
position: data.position || null,
phone: data.phone || null,
email: data.email || null,
companyName: data.companyName || null,
website: data.website || null,
isEnabled: data.isEnabled !== undefined ? data.isEnabled : true,
})
.returning();
return created;
}
};
/**
* Toggle signature enabled/disabled
*/
export const toggleSignature = async (userId, isEnabled) => {
const existing = await getSignature(userId);
if (!existing) {
throw new NotFoundError('Signature not found');
}
const [updated] = await db
.update(emailSignatures)
.set({
isEnabled,
updatedAt: new Date(),
})
.where(eq(emailSignatures.userId, userId))
.returning();
return updated;
};
/**
* Format signature as plain text for email
*/
export const formatSignatureText = (signature) => {
if (!signature || !signature.isEnabled) {
return '';
}
const lines = [];
if (signature.fullName) {
lines.push(signature.fullName);
}
const positionLine = [signature.position, signature.companyName]
.filter(Boolean)
.join(' | ');
if (positionLine) {
lines.push(positionLine);
}
if (signature.phone) {
lines.push(signature.phone);
}
if (signature.email) {
lines.push(signature.email);
}
if (signature.website) {
lines.push(signature.website);
}
if (lines.length === 0) {
return '';
}
return '\n\n———\n' + lines.join('\n');
};
/**
* Delete signature for a user
*/
export const deleteSignature = async (userId) => {
const existing = await getSignature(userId);
if (!existing) {
throw new NotFoundError('Signature not found');
}
await db
.delete(emailSignatures)
.where(eq(emailSignatures.userId, userId));
return { message: 'Signature deleted' };
};