/** * Validation utilities for user profile fields. * * These validation functions mirror the backend validation logic in * backend/validation.py. Both use shared rules from shared/constants.json * to ensure consistent validation across frontend and backend. */ import { bech32 } from "bech32"; import constants from "../../../shared/constants.json"; const { telegram: telegramRules, signal: signalRules, nostrNpub: npubRules } = constants.validation; /** * Validate contact email format. * Returns undefined if valid, error message if invalid. * Empty values are valid (field is optional). */ export function validateEmail(value: string): string | undefined { if (!value) return undefined; // Comprehensive email regex that matches email-validator behavior // Checks for: local part, @, domain with at least one dot, valid TLD const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/; if (!emailRegex.test(value)) { return "Please enter a valid email address"; } return undefined; } /** * Validate Telegram handle. * Must start with @ if provided, with characters after @ within max length. * Returns undefined if valid, error message if invalid. * Empty values are valid (field is optional). */ export function validateTelegram(value: string): string | undefined { if (!value) return undefined; if (!value.startsWith(telegramRules.mustStartWith)) { return `Telegram handle must start with ${telegramRules.mustStartWith}`; } const handle = value.slice(1); if (handle.length < 1) { return `Telegram handle must have at least one character after ${telegramRules.mustStartWith}`; } if (handle.length > telegramRules.maxLengthAfterAt) { return `Telegram handle must be at most ${telegramRules.maxLengthAfterAt} characters (after ${telegramRules.mustStartWith})`; } return undefined; } /** * Validate Signal username. * Any non-empty string within max length is valid. * Returns undefined if valid, error message if invalid. * Empty values are valid (field is optional). */ export function validateSignal(value: string): string | undefined { if (!value) return undefined; if (value.trim().length === 0) { return "Signal username cannot be empty"; } if (value.length > signalRules.maxLength) { return `Signal username must be at most ${signalRules.maxLength} characters`; } return undefined; } /** * Validate Nostr npub (public key in bech32 format). * Must be valid bech32 with 'npub' prefix. * Returns undefined if valid, error message if invalid. * Empty values are valid (field is optional). */ export function validateNostrNpub(value: string): string | undefined { if (!value) return undefined; if (!value.startsWith(npubRules.prefix)) { return `Nostr npub must start with '${npubRules.prefix}'`; } try { const decoded = bech32.decode(value); if (decoded.prefix !== "npub") { return "Nostr npub must have 'npub' prefix"; } // npub should decode to 32 bytes (256 bits) for a public key // In bech32, each character encodes 5 bits, so 32 bytes = 52 characters of data if (decoded.words.length !== npubRules.bech32Words) { return "Invalid Nostr npub: incorrect length"; } return undefined; } catch { return "Invalid Nostr npub: bech32 checksum failed"; } } /** * Field errors object type. */ export interface FieldErrors { contact_email?: string; telegram?: string; signal?: string; nostr_npub?: string; } /** * Validate all profile fields at once. * Returns an object with field_name -> error_message for any invalid fields. * Empty object means all fields are valid. */ export function validateProfileFields(data: { contact_email?: string; telegram?: string; signal?: string; nostr_npub?: string; }): FieldErrors { const errors: FieldErrors = {}; const emailError = validateEmail(data.contact_email || ""); if (emailError) errors.contact_email = emailError; const telegramError = validateTelegram(data.telegram || ""); if (telegramError) errors.telegram = telegramError; const signalError = validateSignal(data.signal || ""); if (signalError) errors.signal = signalError; const npubError = validateNostrNpub(data.nostr_npub || ""); if (npubError) errors.nostr_npub = npubError; return errors; }