first round of review

This commit is contained in:
counterweight 2025-12-19 10:30:23 +01:00
parent 5908660e56
commit 7140cf6f27
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
9 changed files with 61 additions and 63 deletions

View file

@ -2,6 +2,7 @@
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { bech32 } from "bech32";
import { useAuth } from "../auth-context";
import { API_URL } from "../config";
import { sharedStyles } from "../styles/shared";
@ -30,7 +31,9 @@ interface FieldErrors {
// Client-side validation matching backend rules
function validateEmail(value: string): string | undefined {
if (!value) return undefined;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// More 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";
}
@ -43,15 +46,12 @@ function validateTelegram(value: string): string | undefined {
return "Telegram handle must start with @";
}
const handle = value.slice(1);
if (handle.length < 5) {
return "Telegram handle must be at least 5 characters (after @)";
if (handle.length < 1) {
return "Telegram handle must have at least one character after @";
}
if (handle.length > 32) {
return "Telegram handle must be at most 32 characters (after @)";
}
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(handle)) {
return "Telegram handle must start with a letter and contain only letters, numbers, and underscores";
}
return undefined;
}
@ -71,15 +71,21 @@ function validateNostrNpub(value: string): string | undefined {
if (!value.startsWith("npub1")) {
return "Nostr npub must start with 'npub1'";
}
// Basic length check (valid npubs are 63 characters)
if (value.length !== 63) {
return "Invalid Nostr npub format";
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 !== 52) {
return "Invalid Nostr npub: incorrect length";
}
return undefined;
} catch {
return "Invalid Nostr npub: bech32 checksum failed";
}
// Check for valid bech32 characters
if (!/^npub1[023456789acdefghjklmnpqrstuvwxyz]+$/.test(value)) {
return "Invalid Nostr npub: contains invalid characters";
}
return undefined;
}
function validateForm(data: FormData): FieldErrors {
@ -138,21 +144,7 @@ export default function ProfilePage() {
}
}, [isLoading, user, router, isRegularUser]);
useEffect(() => {
if (user && isRegularUser) {
fetchProfile();
}
}, [user, isRegularUser]);
// Auto-dismiss toast after 3 seconds
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
const fetchProfile = async () => {
const fetchProfile = useCallback(async () => {
try {
const res = await fetch(`${API_URL}/api/profile`, {
credentials: "include",
@ -167,13 +159,29 @@ export default function ProfilePage() {
};
setFormData(formValues);
setOriginalData(formValues);
} else {
setToast({ message: "Failed to load profile", type: "error" });
}
} catch {
// Handle error silently for now
setToast({ message: "Network error. Please try again.", type: "error" });
} finally {
setIsLoadingProfile(false);
}
};
}, []);
useEffect(() => {
if (user && isRegularUser) {
fetchProfile();
}
}, [user, isRegularUser, fetchProfile]);
// Auto-dismiss toast after 3 seconds
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
const handleInputChange = (field: keyof FormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

View file

@ -207,7 +207,7 @@ test.describe("Profile - Form Behavior", () => {
await expect(page.getByText(/saved successfully/i)).toBeVisible();
// Wait for toast to disappear
await page.waitForTimeout(3500);
await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 });
// Clear the field
await page.fill("#telegram", "");

View file

@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.1.0",
"dependencies": {
"bech32": "^2.0.0",
"next": "15.1.2",
"react": "19.0.0",
"react-dom": "19.0.0"
@ -2092,6 +2093,12 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bech32": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==",
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",

View file

@ -10,6 +10,7 @@
"test:e2e": "playwright test"
},
"dependencies": {
"bech32": "^2.0.0",
"next": "15.1.2",
"react": "19.0.0",
"react-dom": "19.0.0"