aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaster4385 <blaster4385@tablaster.dev>2024-05-15 13:00:19 +0530
committerBlaster4385 <blaster4385@tablaster.dev>2024-05-15 13:15:00 +0530
commitafab9091b1cb377516222e08d668db1020dcd60e (patch)
tree138b301d8313c3bb20c07d3f1697a3888e8e751d
parent4c1b757403b676287eab222248fd38cc6e18adba (diff)
refactor: optimized editor, encryption and backend code
-rw-r--r--client/src/components/Editor/Editor.jsx210
-rw-r--r--client/src/utils/constants.js1
-rw-r--r--client/src/utils/encryption.js29
-rw-r--r--server/main.go73
4 files changed, 138 insertions, 175 deletions
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 = () => {
<Modal
openModal={openModal}
setOpenModal={setOpenModal}
- onSuccessClick={() => {
- handleSuccessClick();
- }}
- onCancelClick={() => {
- handleCancelClick();
- }}
+ onSuccessClick={handleSuccessClick}
+ onCancelClick={handleCancelClick}
/>
<div className={styles.container}>
{!id && (
- <button
- className={styles.btn__save}
- onClick={() => {
- handleSaveClick();
- }}
- >
- <img src="assets/icons/save.svg" className={styles.btn__icon} />
+ <button className={styles.btn__save} onClick={handleSaveClick}>
+ <img
+ src="assets/icons/save.svg"
+ className={styles.btn__icon}
+ alt="Save"
+ />
</button>
)}
@@ -245,8 +219,9 @@ const Editor = () => {
spellCheck="false"
ref={textareaRef}
placeholder="</> Paste, save, share! (Pasting just a URL will shorten it!)"
+ value={text}
/>
- <pre className="line-numbers">
+ <pre className="line-numbers" ref={lineNumberRef}>
<code
className={`${styles.codespace__code} language-${language}`}
>
@@ -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
+var (
+ db *sql.DB
+ dbFilePath string
+ port string
+ shortIDCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ shortIDLength = 8
+)
type Bin struct {
Content string `json:"content"`
Language string `json:"language"`
- IV string `json:"iv"`
+ IV string `json:"iv"`
}
const (
- shortIDCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- shortIDLength = 8
+ insertQuery = "INSERT INTO bins (id, content, language, iv) VALUES (?, ?, ?, ?)"
+ selectQuery = "SELECT content, language, iv FROM bins WHERE id = ?"
)
-var (
- //go:embed all:dist
- dist embed.FS
-)
+//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))]