Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ const createApp = () =>
}

let { alias, original_url } = body;
original_url = original_url?.trim() ?? "";
if (!original_url) {
return errorResponse("URL is required", 400);
}

if (!HAS_URI_SCHEME_PATTERN.test(original_url)) {
original_url = `https://${original_url}`;
}
Expand Down
17 changes: 14 additions & 3 deletions src/client/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { z } from "zod";
import { ALIAS_MAX_LENGTH } from "./constants";

const HAS_URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z\d+\-.]*:\/\//;

export const normalizeUrlInput = (url: string): string => {
const trimmed = url.trim();
if (!trimmed) return trimmed;
if (HAS_URI_SCHEME_PATTERN.test(trimmed)) return trimmed;
return `https://${trimmed}`;
};

const isIpAddress = (hostname: string): boolean => {
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) return true;
if (hostname.includes(":") || hostname.startsWith("[")) return true;
Expand All @@ -9,9 +18,11 @@ const isIpAddress = (hostname: string): boolean => {

export const urlSchema = z.object({
url: z
.string()
.url("Invalid URL")
.refine((url) => url.startsWith("https://"), {
.preprocess(
(value) => (typeof value === "string" ? normalizeUrlInput(value) : value),
z.string().url("Invalid URL"),
)
.refine((url) => url.toLowerCase().startsWith("https://"), {
message: "Only HTTPS URLs are allowed",
})
.refine(
Expand Down
29 changes: 11 additions & 18 deletions src/client/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@ import {
downloadQRCodeFromDataUrl,
generateQRCodePNG,
} from "../../api/lib/qrcode";
import { ThemeToggle } from "../components/ThemeToggle";
import { Toast } from "../components/Toast";
import {
ArrowRightIcon,
CopyIcon,
InfoIcon,
LightningLogo,
QRIcon,
} from "../components/icons";
import { ThemeToggle } from "../components/ThemeToggle";
import { Toast } from "../components/Toast";
import { useDecryptAnimation } from "../hooks/useDecryptAnimation";
import { useSyncStatus } from "../hooks/useSyncStatus";
import { useUrlShortener } from "../hooks/useUrlShortener";
import { ALIAS_MAX_LENGTH, getBaseUrl, getShortUrl } from "../lib/constants";
import { isOnline } from "../lib/sync";
import { useTheme } from "../lib/useTheme";
import { isValidAlias, urlSchema, type UrlFormData } from "../lib/validation";
import {
isValidAlias,
normalizeUrlInput,
type UrlFormData,
urlSchema,
} from "../lib/validation";

function HomePage() {
const { isDarkMode, toggleTheme } = useTheme();
Expand Down Expand Up @@ -87,7 +92,6 @@ function HomePage() {
setValue,
watch,
clearErrors,
setError,
} = useForm<UrlFormData>({
resolver: zodResolver(urlSchema),
mode: "onSubmit",
Expand Down Expand Up @@ -123,24 +127,13 @@ function HomePage() {
const handlePaste = useCallback(
(e: React.ClipboardEvent<HTMLInputElement>) => {
const pastedText = e.clipboardData.getData("text").trim();

if (
pastedText.startsWith("http://") ||
pastedText.startsWith("https://")
) {
if (pastedText.length > 0) {
e.preventDefault();
setValue("url", pastedText);
setValue("url", normalizeUrlInput(pastedText));
setTimeout(() => handleSubmit(onSubmit)(), 100);
} else if (pastedText.length > 0) {
e.preventDefault();
setValue("url", pastedText);
setError("url", {
type: "manual",
message: "Only HTTPS URLs are allowed",
});
}
},
[setValue, handleSubmit, onSubmit, setError],
[setValue, handleSubmit, onSubmit],
);

const watchedUrl = watch("url");
Expand Down