implemented

This commit is contained in:
counterweight 2025-12-20 23:06:05 +01:00
parent a31bd8246c
commit d3638e2e69
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
18 changed files with 1643 additions and 120 deletions

View file

@ -6,15 +6,13 @@ import { api, ApiError } from "../api";
import { sharedStyles } from "../styles/shared";
import { Header } from "../components/Header";
import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
interface ProfileData {
contact_email: string | null;
telegram: string | null;
signal: string | null;
nostr_npub: string | null;
godfather_email: string | null;
}
// Use generated type from OpenAPI schema
type ProfileData = components["schemas"]["ProfileResponse"];
// UI-specific types (not from API)
interface FormData {
contact_email: string;
telegram: string;
@ -29,7 +27,9 @@ interface FieldErrors {
nostr_npub?: string;
}
// Client-side validation matching backend rules
// Client-side validation using shared rules from constants
const { telegram: telegramRules, signal: signalRules, nostrNpub: npubRules } = constants.validation;
function validateEmail(value: string): string | undefined {
if (!value) return undefined;
// More comprehensive email regex that matches email-validator behavior
@ -43,15 +43,15 @@ function validateEmail(value: string): string | undefined {
function validateTelegram(value: string): string | undefined {
if (!value) return undefined;
if (!value.startsWith("@")) {
return "Telegram handle must start with @";
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 @";
return `Telegram handle must have at least one character after ${telegramRules.mustStartWith}`;
}
if (handle.length > 32) {
return "Telegram handle must be at most 32 characters (after @)";
if (handle.length > telegramRules.maxLengthAfterAt) {
return `Telegram handle must be at most ${telegramRules.maxLengthAfterAt} characters (after ${telegramRules.mustStartWith})`;
}
return undefined;
}
@ -61,16 +61,16 @@ function validateSignal(value: string): string | undefined {
if (value.trim().length === 0) {
return "Signal username cannot be empty";
}
if (value.length > 64) {
return "Signal username must be at most 64 characters";
if (value.length > signalRules.maxLength) {
return `Signal username must be at most ${signalRules.maxLength} characters`;
}
return undefined;
}
function validateNostrNpub(value: string): string | undefined {
if (!value) return undefined;
if (!value.startsWith("npub1")) {
return "Nostr npub must start with 'npub1'";
if (!value.startsWith(npubRules.prefix)) {
return `Nostr npub must start with '${npubRules.prefix}'`;
}
try {
@ -80,7 +80,7 @@ function validateNostrNpub(value: string): string | undefined {
}
// 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) {
if (decoded.words.length !== npubRules.bech32Words) {
return "Invalid Nostr npub: incorrect length";
}
return undefined;
@ -113,7 +113,7 @@ function toFormData(data: ProfileData): FormData {
export default function ProfilePage() {
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredRole: "regular",
requiredRole: constants.roles.REGULAR,
fallbackRedirect: "/audit",
});
const [originalData, setOriginalData] = useState<FormData | null>(null);
@ -152,7 +152,7 @@ export default function ProfilePage() {
const formValues = toFormData(data);
setFormData(formValues);
setOriginalData(formValues);
setGodfatherEmail(data.godfather_email);
setGodfatherEmail(data.godfather_email ?? null);
} catch (err) {
console.error("Profile load error:", err);
setToast({ message: "Failed to load profile", type: "error" });