a4051ae132
Next.js 16 dashboard with configurable widgets (favorites, notes, calendar, clock, calculator, search, domain-check), multi-tab support, user auth, dark mode, and Docker deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
229 lines
4.5 KiB
TypeScript
229 lines
4.5 KiB
TypeScript
export function normalizeEmail(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const email = value.trim().toLowerCase();
|
|
|
|
if (!email.includes("@") || email.length > 254) {
|
|
return null;
|
|
}
|
|
|
|
return email;
|
|
}
|
|
|
|
export function normalizePassword(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
if (value.length < 10 || value.length > 128) {
|
|
return null;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
export function normalizeDisplayName(value: unknown): string | null {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const displayName = value.trim();
|
|
|
|
if (displayName.length < 1 || displayName.length > 80) {
|
|
return null;
|
|
}
|
|
|
|
return displayName;
|
|
}
|
|
|
|
export function normalizeUserRole(value: unknown): "ADMIN" | "USER" | null {
|
|
if (value === "ADMIN" || value === "USER") {
|
|
return value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function normalizeDashboardTitle(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const title = value.trim();
|
|
|
|
if (title.length < 1 || title.length > 80) {
|
|
return null;
|
|
}
|
|
|
|
return title;
|
|
}
|
|
|
|
export function normalizeOptionalDashboardSubtitle(value: unknown): string | null {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const subtitle = value.trim();
|
|
|
|
if (subtitle.length > 120) {
|
|
return null;
|
|
}
|
|
|
|
return subtitle.length > 0 ? subtitle : null;
|
|
}
|
|
|
|
export function normalizeThemeColor(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const color = value.trim();
|
|
|
|
const shortHexMatch = /^#([0-9a-fA-F]{3})$/.exec(color);
|
|
|
|
if (shortHexMatch) {
|
|
const [r, g, b] = shortHexMatch[1].split("");
|
|
|
|
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
}
|
|
|
|
if (/^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
return color.toLowerCase();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function normalizeOptionalLogoUrl(value: unknown): string | null {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
|
|
if (trimmed.length < 1 || trimmed.length > 2048) {
|
|
return null;
|
|
}
|
|
|
|
if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
|
|
return trimmed;
|
|
}
|
|
|
|
try {
|
|
const url = new URL(trimmed);
|
|
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
return null;
|
|
}
|
|
|
|
return url.toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function normalizeOptionalImageUrl(value: unknown): string | null {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
|
|
if (trimmed.length < 1 || trimmed.length > 2048) {
|
|
return null;
|
|
}
|
|
|
|
if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
|
|
return trimmed;
|
|
}
|
|
|
|
try {
|
|
const url = new URL(trimmed);
|
|
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
return null;
|
|
}
|
|
|
|
return url.toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function normalizeTitle(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const title = value.trim();
|
|
|
|
if (title.length < 1 || title.length > 80) {
|
|
return null;
|
|
}
|
|
|
|
return title;
|
|
}
|
|
|
|
export function normalizeUrl(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
|
|
if (trimmed.length < 3 || trimmed.length > 2048) {
|
|
return null;
|
|
}
|
|
|
|
const withProtocol =
|
|
trimmed.startsWith("http://") || trimmed.startsWith("https://") ? trimmed : `https://${trimmed}`;
|
|
|
|
try {
|
|
const url = new URL(withProtocol);
|
|
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
return null;
|
|
}
|
|
|
|
return url.toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function normalizeOptionalUrl(value: unknown): string | null {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
return normalizeUrl(value);
|
|
}
|
|
|
|
export function normalizePositiveInteger(value: unknown, fallback: number, min: number, max: number): number {
|
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
|
|
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
return fallback;
|
|
}
|
|
|
|
return parsed;
|
|
}
|