Initial commit: Personal Dashboard
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>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user