diff --git a/client/src/components/Editor/Editor.jsx b/client/src/components/Editor/Editor.jsx index e1675fc..dac633c 100644 --- a/client/src/components/Editor/Editor.jsx +++ b/client/src/components/Editor/Editor.jsx @@ -1,10 +1,16 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { + useState, + useEffect, + useRef, + useCallback, + useMemo, +} from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import Prism from "prismjs"; import styles from "./Editor.module.css"; import "../prism-themes/prism-gruvbox-dark.css"; import "../prism-themes/prism-line-numbers.css"; -import { BASE_URL, URL_REGEX } from "../../utils/constants"; +import { URL_REGEX } from "../../utils/constants"; import Header from "../Header/Header"; import { generateAESKey, @@ -24,25 +30,29 @@ const Editor = () => { const [openModal, setOpenModal] = useState(false); const textareaRef = useRef(null); const lineNumberRef = useRef(null); - const queryParams = new URLSearchParams(location.search); + const queryParams = useMemo( + () => new URLSearchParams(location.search), + [location.search], + ); + const origin = useMemo(() => window.location.origin, []); - const handleTextChange = (event) => { + const handleTextChange = useCallback((event) => { setText(event.target.value); - }; + }, []); - const handleScroll = () => { + const handleScroll = useCallback(() => { if (textareaRef.current && lineNumberRef.current) { lineNumberRef.current.scrollTop = textareaRef.current.scrollTop; } - }; + }, []); - const handleSaveClick = async () => { + const handleSaveClick = useCallback(async () => { if (!text) { alert("Please enter some text!"); return; } if (URL_REGEX.test(text)) { - const response = await fetch(`${BASE_URL}/bin`, { + const response = await fetch(`${origin}/bin`, { method: "POST", headers: { "Content-Type": "application/json", @@ -54,29 +64,19 @@ const Editor = () => { }); const data = await response.json(); if (response.ok) { - navigator.clipboard - .writeText(`${window.location.origin}/r/${data.id}`) - .then( - function () { - alert("Short URL copied to clipboard!"); - }, - function () { - try { - document.execCommand("copy"); - alert("Short URL copied to clipboard!"); - } catch (err) { - console.log("Oops, unable to copy"); - } - }, - ); + const shortURL = `${origin}/r/${data.id}`; + copyToClipboard(shortURL); + alert("Short URL copied to clipboard!"); + navigate(`/${data.id}`); + } else { + console.error(data); } - navigate(`/${data.id}`); - return; + } else { + setOpenModal(true); } - setOpenModal(true); - }; + }, [text, language, navigate]); - const handleSuccessClick = async () => { + const handleSuccessClick = useCallback(async () => { setOpenModal(false); const key = await generateAESKey(); const keyString = await keyToString(key); @@ -86,7 +86,7 @@ const Editor = () => { ); const ivBase64 = btoa(String.fromCharCode.apply(null, iv)); - const response = await fetch(`${BASE_URL}/bin`, { + const response = await fetch(`${origin}/bin`, { method: "POST", headers: { "Content-Type": "application/json", @@ -99,33 +99,18 @@ const Editor = () => { }); const data = await response.json(); if (response.ok) { - navigator.clipboard - .writeText(`${window.location.origin}/${data.id}?key=${keyString}`) - .then( - function () { - navigator.clipboard.writeText( - `${window.location.origin}/${data.id}?key=${keyString}`, - ); - alert("URL copied to clipboard!"); - }, - function () { - try { - document.execCommand("copy"); - alert("URL copied to clipboard!"); - } catch (err) { - console.log("Oops, unable to copy"); - } - }, - ); + const encryptedURL = `${origin}/${data.id}?key=${keyString}`; + copyToClipboard(encryptedURL); + alert("URL copied to clipboard!"); navigate(`/${data.id}?key=${keyString}`); } else { console.error(data); } - }; + }, [text, language, navigate]); - const handleCancelClick = async () => { + const handleCancelClick = useCallback(async () => { setOpenModal(false); - const response = await fetch(`${BASE_URL}/bin`, { + const response = await fetch(`${origin}/bin`, { method: "POST", headers: { "Content-Type": "application/json", @@ -137,78 +122,72 @@ const Editor = () => { }); const data = await response.json(); if (response.ok) { - navigator.clipboard - .writeText(`${window.location.origin}/${data.id}`) - .then( - function () { - navigator.clipboard.writeText( - `${window.location.origin}/${data.id}`, - ); - alert("URL copied to clipboard!"); - }, - function () { - try { - document.execCommand("copy"); - alert("URL copied to clip`board!"); - } catch (err) { - console.log("Oops, unable to copy"); - } - }, - ); + const normalURL = `${origin}/${data.id}`; + copyToClipboard(normalURL); + alert("URL copied to clipboard!"); navigate(`/${data.id}`); } else { console.error(data); } - }; + }, [text, language, navigate]); - const handleLanguageChange = (value) => { + const handleLanguageChange = useCallback((value) => { setLanguage(value); - }; + }, []); useEffect(() => { Prism.highlightAll(); }, [text, language]); - useEffect(() => { - const fetchData = async () => { - const response = await fetch(`${BASE_URL}/bin/${id}`); - const data = await response.json(); - if (response.ok) { - if (data.iv) { - const keyString = queryParams.get("key"); - const key = await stringToKey(keyString); - const encrypted = new Uint8Array( - atob(data.content) - .split("") - .map((char) => char.charCodeAt(0)), - ).buffer; - const ivArray = new Uint8Array( - atob(data.iv) - .split("") - .map((char) => char.charCodeAt(0)), - ); - const decryptedContent = await decryptAES(encrypted, key, ivArray); - setLanguage(data.language); - setText(decryptedContent); + const fetchData = useCallback(async () => { + const response = await fetch(`${origin}/bin/${id}`); + const data = await response.json(); + if (response.ok) { + if (data.iv) { + const keyString = queryParams.get("key"); + const key = await stringToKey(keyString); + const encrypted = new Uint8Array( + atob(data.content) + .split("") + .map((char) => char.charCodeAt(0)), + ).buffer; + const ivArray = new Uint8Array( + atob(data.iv) + .split("") + .map((char) => char.charCodeAt(0)), + ); + const decryptedContent = await decryptAES(encrypted, key, ivArray); + setLanguage(data.language); + setText(decryptedContent); + } else { + const isURL = URL_REGEX.test(data.content); + if (isURL) { + setText(`Your shortened URL: ${origin}/r/${id}`); } else { - const isURL = URL_REGEX.test(data.content); - if (isURL) { - setText(`Your shortened URL: ${window.location.origin}/r/${id}`); - } else { - setLanguage(data.language); - setText(data.content); - } + setLanguage(data.language); + setText(data.content); } } - }; + } + }, [id, queryParams]); + useEffect(() => { if (id) { fetchData(); } else { - textareaRef.current.value = ""; setText(""); } - }, [id]); + }, [id, fetchData]); + + const copyToClipboard = useCallback((text) => { + navigator.clipboard.writeText(text).catch(() => { + try { + document.execCommand("copy"); + } catch (err) { + console.log("Oops, unable to copy"); + } + }); + }, []); return ( <> @@ -216,22 +195,17 @@ const Editor = () => { { - handleSuccessClick(); - }} - onCancelClick={() => { - handleCancelClick(); - }} + onSuccessClick={handleSuccessClick} + onCancelClick={handleCancelClick} />
{!id && ( - )} @@ -245,8 +219,9 @@ const Editor = () => { spellCheck="false" ref={textareaRef} placeholder=" Paste, save, share! (Pasting just a URL will shorten it!)" + value={text} /> -
+            
               
@@ -261,4 +236,3 @@ const Editor = () => {
 };
 
 export default Editor;
-
diff --git a/client/src/utils/constants.js b/client/src/utils/constants.js
index e783114..d2693fd 100644
--- a/client/src/utils/constants.js
+++ b/client/src/utils/constants.js
@@ -1,4 +1,3 @@
-export const BASE_URL = window.location.origin;
 export const URL_REGEX = /^(https?:\/\/)?([\w.-]+\.[a-z]{2,})(\/?[^\s]*)?$/;
 export const SUPPORTED_LANGUAGES = [
   {
diff --git a/client/src/utils/encryption.js b/client/src/utils/encryption.js
index e0e6c33..3242422 100644
--- a/client/src/utils/encryption.js
+++ b/client/src/utils/encryption.js
@@ -1,26 +1,26 @@
 async function generateAESKey() {
   try {
     const key = await window.crypto.subtle.generateKey(
-      {
-        name: "AES-GCM",
-        length: 256,
-      },
+      { name: "AES-GCM", length: 256 },
       true,
       ["encrypt", "decrypt"],
     );
     return key;
   } catch (error) {
     console.error("Error generating AES key:", error);
+    throw error;
   }
 }
 
 async function keyToString(key) {
   try {
     const exportedKey = await window.crypto.subtle.exportKey("raw", key);
-    const keyString = btoa(String.fromCharCode(...new Uint8Array(exportedKey)));
-    return keyString.replace(/\+/g, "-").replace(/\//g, "_");
+    return btoa(String.fromCharCode(...new Uint8Array(exportedKey)))
+      .replace(/\+/g, "-")
+      .replace(/\//g, "_");
   } catch (error) {
     console.error("Error converting key to string:", error);
+    throw error;
   }
 }
 
@@ -31,17 +31,16 @@ async function stringToKey(keyString) {
       c.charCodeAt(0),
     ).buffer;
 
-    const key = await window.crypto.subtle.importKey(
+    return await window.crypto.subtle.importKey(
       "raw",
       buffer,
       { name: "AES-GCM", length: 256 },
       true,
       ["encrypt", "decrypt"],
     );
-
-    return key;
   } catch (error) {
     console.error("Error converting string to key:", error);
+    throw error;
   }
 }
 
@@ -49,32 +48,28 @@ async function encryptAES(plaintext, key) {
   try {
     const iv = window.crypto.getRandomValues(new Uint8Array(12));
     const encrypted = await window.crypto.subtle.encrypt(
-      {
-        name: "AES-GCM",
-        iv: iv,
-      },
+      { name: "AES-GCM", iv: iv },
       key,
       new TextEncoder().encode(plaintext),
     );
     return { encrypted, iv };
   } catch (error) {
     console.error("Error encrypting:", error);
+    throw error;
   }
 }
 
 async function decryptAES(encrypted, key, iv) {
   try {
     const decrypted = await window.crypto.subtle.decrypt(
-      {
-        name: "AES-GCM",
-        iv: iv,
-      },
+      { name: "AES-GCM", iv: iv },
       key,
       encrypted,
     );
     return new TextDecoder().decode(decrypted);
   } catch (error) {
     console.error("Error decrypting:", error);
+    throw error;
   }
 }
 
diff --git a/server/main.go b/server/main.go
index 5aaeba7..92bce1d 100644
--- a/server/main.go
+++ b/server/main.go
@@ -7,37 +7,36 @@ import (
 	"log"
 	"math/rand"
 	"net/http"
-	"time"
 
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
-
 	_ "github.com/mattn/go-sqlite3"
 )
 
-var db *sql.DB
-var dbFilePath string
-var port string
-
-type Bin struct {
-	Content  string `json:"content"`
-	Language string `json:"language"`
-  IV string `json:"iv"`
-}
-
-const (
+var (
+	db             *sql.DB
+	dbFilePath     string
+	port           string
 	shortIDCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 	shortIDLength  = 8
 )
 
-var (
-	//go:embed all:dist
-	dist embed.FS
+type Bin struct {
+	Content  string `json:"content"`
+	Language string `json:"language"`
+	IV       string `json:"iv"`
+}
+
+const (
+	insertQuery = "INSERT INTO bins (id, content, language, iv) VALUES (?, ?, ?, ?)"
+	selectQuery = "SELECT content, language, iv FROM bins WHERE id = ?"
 )
 
+//go:embed all:dist
+var dist embed.FS
+
 func RegisterHandlers(e *echo.Echo) {
 	e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
-		Skipper:    nil,
 		Root:       "dist",
 		Index:      "index.html",
 		HTML5:      true,
@@ -55,6 +54,7 @@ func main() {
 	flag.Parse()
 
 	initDatabase()
+
 	e := echo.New()
 	RegisterHandlers(e)
 	e.Logger.Fatal(e.Start(":" + port))
@@ -67,47 +67,43 @@ func initDatabase() {
 		log.Fatal(err)
 	}
 
-	err = createTable()
-	if err != nil {
+	if err := createTable(); err != nil {
 		log.Fatal(err)
 	}
 }
 
-func postBin(echoContext echo.Context) error {
-	bin := Bin{}
-	err := echoContext.Bind(&bin)
-	if err != nil {
+func postBin(c echo.Context) error {
+	var bin Bin
+	if err := c.Bind(&bin); err != nil {
 		return err
 	}
+
 	id := generateShortID()
-	err = saveBin(id, bin)
-	if err != nil {
+	if err := saveBin(id, bin); err != nil {
 		return err
 	}
-	return echoContext.JSON(http.StatusCreated, echo.Map{
-		"id": id,
-	})
+
+	return c.JSON(http.StatusCreated, echo.Map{"id": id})
 }
 
-func getBin(echoContext echo.Context) error {
-	id := echoContext.Param("id")
-	bin, err := getBinById(id)
+func getBin(c echo.Context) error {
+	id := c.Param("id")
+	bin, err := getBinByID(id)
 	if err != nil {
 		return err
 	}
-	return echoContext.JSON(http.StatusOK, bin)
+	return c.JSON(http.StatusOK, bin)
 }
 
-func redirectToURL(echoContext echo.Context) error {
-	id := echoContext.Param("id")
-	bin, err := getBinById(id)
+func redirectToURL(c echo.Context) error {
+	id := c.Param("id")
+	bin, err := getBinByID(id)
 	if err != nil {
-		echoContext.Logger().Error(err)
+		c.Logger().Error(err)
 		return err
 	}
 
-	url := bin.Content
-	return echoContext.Redirect(http.StatusFound, url)
+	return c.Redirect(http.StatusFound, bin.Content)
 }
 
 func createTable() error {
@@ -115,20 +111,19 @@ func createTable() error {
 	return err
 }
 
-func getBinById(id string) (Bin, error) {
-	row := db.QueryRow("SELECT content, language, iv FROM bins WHERE id = ?", id)
-	bin := Bin{}
+func getBinByID(id string) (Bin, error) {
+	var bin Bin
+	row := db.QueryRow(selectQuery, id)
 	err := row.Scan(&bin.Content, &bin.Language, &bin.IV)
 	return bin, err
 }
 
 func saveBin(id string, bin Bin) error {
-	_, err := db.Exec("INSERT INTO bins (id, content, language, iv) VALUES (?, ?, ?, ?)", id, bin.Content, bin.Language, bin.IV)
+	_, err := db.Exec(insertQuery, id, bin.Content, bin.Language, bin.IV)
 	return err
 }
 
 func generateShortID() string {
-	rand.Seed(time.Now().UnixNano())
 	id := make([]byte, shortIDLength)
 	for i := range id {
 		id[i] = shortIDCharset[rand.Intn(len(shortIDCharset))]