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 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) => {
|
||||||
|
|||||||
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(),
|
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(),
|
||||||
|
|||||||
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