- 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>
176 lines
5.1 KiB
TypeScript
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)`
|
|
};
|
|
}
|