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>
This commit is contained in:
175
web-app/app/lib/actions/emailActions.ts
Normal file
175
web-app/app/lib/actions/emailActions.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
'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)`
|
||||
};
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export interface UserSettings {
|
||||
ownerRevolutProfileName?: string | null;
|
||||
};
|
||||
|
||||
enum EmailStatus {
|
||||
export enum EmailStatus {
|
||||
/** Email is not yet verified - recipient has not yet confirmed their email address */
|
||||
Unverified = "unverified",
|
||||
/** Email is not yet verified - a verification request has been sent */
|
||||
|
||||
Reference in New Issue
Block a user