Files
evidencija-rezija/web-app/app/lib/actions/emailActions.ts
Knee Cola c1d3026f4b feat: implement email verification and unsubscribe DB logic
- Export EmailStatus enum from db-types.ts
- Add verifyTenantEmail server action
- Add unsubscribeTenantEmail server action
- Both actions update current and all subsequent matching locations
- Match criteria: userId, name, tenantEmail, yearMonth >= current
- Share-id validation using existing shareChecksum utilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 18:13:41 +01:00

176 lines
5.1 KiB
TypeScript

'use server';
import { getDbClient } from '../dbClient';
import { BillingLocation, EmailStatus } from '../db-types';
import { extractShareId, validateShareChecksum } from '../shareChecksum';
import { revalidatePath } from 'next/cache';
export type EmailActionResult = {
success: boolean;
message?: string;
};
/**
* Verify tenant email address
* Updates the email status to Verified for the location and all subsequent matching locations
*
* @param shareId - The share ID from the verification link (locationId + checksum)
* @returns Result indicating success or failure
*/
export async function verifyTenantEmail(shareId: string): Promise<EmailActionResult> {
// Extract and validate share ID
const extracted = extractShareId(shareId);
if (!extracted) {
return {
success: false,
message: 'Invalid verification link'
};
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
return {
success: false,
message: 'Invalid verification link'
};
}
// Get database client
const dbClient = await getDbClient();
// Fetch the location to get userId, name, tenantEmail, and yearMonth
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { userId: 1, name: 1, tenantEmail: 1, yearMonth: 1 } }
);
if (!location) {
return {
success: false,
message: 'Location not found'
};
}
if (!location.tenantEmail) {
return {
success: false,
message: 'No tenant email configured for this location'
};
}
// Update current and all subsequent matching locations
// Match by: userId, name, tenantEmail, and yearMonth >= current
const result = await dbClient.collection<BillingLocation>("lokacije").updateMany(
{
userId: location.userId,
name: location.name,
tenantEmail: location.tenantEmail,
$or: [
{ "yearMonth.year": { $gt: location.yearMonth.year } },
{
"yearMonth.year": location.yearMonth.year,
"yearMonth.month": { $gte: location.yearMonth.month }
}
]
},
{
$set: {
tenantEmailStatus: EmailStatus.Verified
}
}
);
// Revalidate paths to refresh UI
revalidatePath('/[locale]', 'layout');
return {
success: true,
message: `Email verified successfully (${result.modifiedCount} location(s) updated)`
};
}
/**
* Unsubscribe tenant from email notifications
* Updates the email status to Unsubscribed for the location and all subsequent matching locations
*
* @param shareId - The share ID from the unsubscribe link (locationId + checksum)
* @returns Result indicating success or failure
*/
export async function unsubscribeTenantEmail(shareId: string): Promise<EmailActionResult> {
// Extract and validate share ID
const extracted = extractShareId(shareId);
if (!extracted) {
return {
success: false,
message: 'Invalid unsubscribe link'
};
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
return {
success: false,
message: 'Invalid unsubscribe link'
};
}
// Get database client
const dbClient = await getDbClient();
// Fetch the location to get userId, name, tenantEmail, and yearMonth
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { userId: 1, name: 1, tenantEmail: 1, yearMonth: 1 } }
);
if (!location) {
return {
success: false,
message: 'Location not found'
};
}
if (!location.tenantEmail) {
return {
success: false,
message: 'No tenant email configured for this location'
};
}
// Update current and all subsequent matching locations
// Match by: userId, name, tenantEmail, and yearMonth >= current
const result = await dbClient.collection<BillingLocation>("lokacije").updateMany(
{
userId: location.userId,
name: location.name,
tenantEmail: location.tenantEmail,
$or: [
{ "yearMonth.year": { $gt: location.yearMonth.year } },
{
"yearMonth.year": location.yearMonth.year,
"yearMonth.month": { $gte: location.yearMonth.month }
}
]
},
{
$set: {
tenantEmailStatus: EmailStatus.Unsubscribed
}
}
);
// Revalidate paths to refresh UI
revalidatePath('/[locale]', 'layout');
return {
success: true,
message: `Unsubscribed successfully (${result.modifiedCount} location(s) updated)`
};
}