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:
@@ -30,6 +30,7 @@ import eventRoutes from './routes/event.routes.js';
|
||||
import messageRoutes from './routes/message.routes.js';
|
||||
import userRoutes from './routes/user.routes.js';
|
||||
import serviceRoutes from './routes/service.routes.js';
|
||||
import emailSignatureRoutes from './routes/email-signature.routes.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -126,6 +127,7 @@ app.use('/api/events', eventRoutes);
|
||||
app.use('/api/messages', messageRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/services', serviceRoutes);
|
||||
app.use('/api/email-signature', emailSignatureRoutes);
|
||||
|
||||
// Basic route
|
||||
app.get('/', (req, res) => {
|
||||
|
||||
93
src/controllers/email-signature.controller.js
Normal file
93
src/controllers/email-signature.controller.js
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -310,6 +310,21 @@ export const services = pgTable('services', {
|
||||
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
|
||||
export const messages = pgTable('messages', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
42
src/routes/email-signature.routes.js
Normal file
42
src/routes/email-signature.routes.js
Normal 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;
|
||||
140
src/services/email-signature.service.js
Normal file
140
src/services/email-signature.service.js
Normal 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' };
|
||||
};
|
||||
Reference in New Issue
Block a user