Add IBAN validation using iban.js library

Implemented optional IBAN validation for the account form profile:
- Installed iban.js package for robust IBAN validation
- Added Zod validation with IBAN.isValid() for format checking
- Normalizes IBAN (removes spaces, uppercase) before storage
- Validates country-specific formats and checksums
- Added validation error messages in English and Croatian

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-11-17 20:23:11 +01:00
parent 86fa6a67fe
commit 216b08c12b
5 changed files with 38 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ import { IntlTemplateFn } from '@/app/i18n';
import { getTranslations, getLocale } from "next-intl/server"; import { getTranslations, getLocale } from "next-intl/server";
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { gotoHomeWithMessage } from './navigationActions'; import { gotoHomeWithMessage } from './navigationActions';
import * as IBAN from 'iban';
export type State = { export type State = {
errors?: { errors?: {
@@ -29,7 +30,17 @@ const FormSchema = (t: IntlTemplateFn) => z.object({
firstName: z.string().optional(), firstName: z.string().optional(),
lastName: z.string().optional(), lastName: z.string().optional(),
address: z.string().optional(), address: z.string().optional(),
iban: z.string().optional(), iban: z.string()
.optional()
.refine(
(val) => {
if (!val || val.trim() === '') return true;
// Remove spaces and validate using iban.js library
const cleaned = val.replace(/\s/g, '').toUpperCase();
return IBAN.isValid(cleaned);
},
{ message: t("iban-invalid") }
),
}); });
/** /**
@@ -73,6 +84,9 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt
const { firstName, lastName, address, iban } = validatedFields.data; const { firstName, lastName, address, iban } = validatedFields.data;
// Normalize IBAN: remove spaces and convert to uppercase
const normalizedIban = iban ? iban.replace(/\s/g, '').toUpperCase() : null;
// Update the user profile in MongoDB // Update the user profile in MongoDB
const dbClient = await getDbClient(); const dbClient = await getDbClient();
const { id: userId } = user; const { id: userId } = user;
@@ -82,7 +96,7 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt
firstName: firstName || null, firstName: firstName || null,
lastName: lastName || null, lastName: lastName || null,
address: address || null, address: address || null,
iban: iban || null, iban: normalizedIban,
}; };
await dbClient.collection<UserProfile>("users") await dbClient.collection<UserProfile>("users")

View File

@@ -149,6 +149,8 @@
"iban-placeholder": "Enter your IBAN", "iban-placeholder": "Enter your IBAN",
"save-button": "Save", "save-button": "Save",
"cancel-button": "Cancel", "cancel-button": "Cancel",
"validation": {} "validation": {
"iban-invalid": "Invalid IBAN format. Please enter a valid IBAN"
}
} }
} }

View File

@@ -148,6 +148,8 @@
"iban-placeholder": "Unesite svoj IBAN", "iban-placeholder": "Unesite svoj IBAN",
"save-button": "Spremi", "save-button": "Spremi",
"cancel-button": "Odbaci", "cancel-button": "Odbaci",
"validation": {} "validation": {
"iban-invalid": "Neispravan IBAN format. Molimo unesite ispravan IBAN."
}
} }
} }

14
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@mui/icons-material": "^7.3.5", "@mui/icons-material": "^7.3.5",
"@mui/material": "^7.3.5", "@mui/material": "^7.3.5",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/iban": "^0.0.35",
"@types/node": "20.5.7", "@types/node": "20.5.7",
"@zxing/browser": "^0.1.4", "@zxing/browser": "^0.1.4",
"@zxing/library": "^0.20.0", "@zxing/library": "^0.20.0",
@@ -20,6 +21,7 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"daisyui": "^4.5.0", "daisyui": "^4.5.0",
"iban": "^0.0.14",
"is-ua-webview": "^1.1.2", "is-ua-webview": "^1.1.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"next": "^14.0.2", "next": "^14.0.2",
@@ -1745,6 +1747,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/iban": {
"version": "0.0.35",
"resolved": "https://registry.npmjs.org/@types/iban/-/iban-0.0.35.tgz",
"integrity": "sha512-BOsp/b0ypIBnZdp1R8aP3n4w7I0n6vcObXtD0OT91lVSdo+Bx4VL26tW3yx1Dr9I4D5H3A27IOZoMVAdVfG4FQ==",
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -5013,6 +5021,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/iban": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/iban/-/iban-0.0.14.tgz",
"integrity": "sha512-+rocNKk+Ga9m8Lr9fTMWd+87JnsBrucm0ZsIx5ROOarZlaDLmd+FKdbtvb0XyoBw9GAFOYG2GuLqoNB16d+p3w==",
"license": "MIT"
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",

View File

@@ -15,6 +15,7 @@
"@mui/icons-material": "^7.3.5", "@mui/icons-material": "^7.3.5",
"@mui/material": "^7.3.5", "@mui/material": "^7.3.5",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/iban": "^0.0.35",
"@types/node": "20.5.7", "@types/node": "20.5.7",
"@zxing/browser": "^0.1.4", "@zxing/browser": "^0.1.4",
"@zxing/library": "^0.20.0", "@zxing/library": "^0.20.0",
@@ -22,6 +23,7 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"daisyui": "^4.5.0", "daisyui": "^4.5.0",
"iban": "^0.0.14",
"is-ua-webview": "^1.1.2", "is-ua-webview": "^1.1.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"next": "^14.0.2", "next": "^14.0.2",