refactor: optimized editor, encryption and backend code

This commit is contained in:
Blaster4385 2024-05-15 13:00:19 +05:30
parent 4c1b757403
commit afab9091b1
4 changed files with 142 additions and 179 deletions

View file

@ -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 { useLocation, useNavigate, useParams } from "react-router-dom";
import Prism from "prismjs"; import Prism from "prismjs";
import styles from "./Editor.module.css"; import styles from "./Editor.module.css";
import "../prism-themes/prism-gruvbox-dark.css"; import "../prism-themes/prism-gruvbox-dark.css";
import "../prism-themes/prism-line-numbers.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 Header from "../Header/Header";
import { import {
generateAESKey, generateAESKey,
@ -24,25 +30,29 @@ const Editor = () => {
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const textareaRef = useRef(null); const textareaRef = useRef(null);
const lineNumberRef = 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); setText(event.target.value);
}; }, []);
const handleScroll = () => { const handleScroll = useCallback(() => {
if (textareaRef.current && lineNumberRef.current) { if (textareaRef.current && lineNumberRef.current) {
lineNumberRef.current.scrollTop = textareaRef.current.scrollTop; lineNumberRef.current.scrollTop = textareaRef.current.scrollTop;
} }
}; }, []);
const handleSaveClick = async () => { const handleSaveClick = useCallback(async () => {
if (!text) { if (!text) {
alert("Please enter some text!"); alert("Please enter some text!");
return; return;
} }
if (URL_REGEX.test(text)) { if (URL_REGEX.test(text)) {
const response = await fetch(`${BASE_URL}/bin`, { const response = await fetch(`${origin}/bin`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -54,29 +64,19 @@ const Editor = () => {
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
navigator.clipboard const shortURL = `${origin}/r/${data.id}`;
.writeText(`${window.location.origin}/r/${data.id}`) copyToClipboard(shortURL);
.then( alert("Short URL copied to clipboard!");
function () { navigate(`/${data.id}`);
alert("Short URL copied to clipboard!"); } else {
}, console.error(data);
function () {
try {
document.execCommand("copy");
alert("Short URL copied to clipboard!");
} catch (err) {
console.log("Oops, unable to copy");
}
},
);
} }
navigate(`/${data.id}`); } else {
return; setOpenModal(true);
} }
setOpenModal(true); }, [text, language, navigate]);
};
const handleSuccessClick = async () => { const handleSuccessClick = useCallback(async () => {
setOpenModal(false); setOpenModal(false);
const key = await generateAESKey(); const key = await generateAESKey();
const keyString = await keyToString(key); const keyString = await keyToString(key);
@ -86,7 +86,7 @@ const Editor = () => {
); );
const ivBase64 = btoa(String.fromCharCode.apply(null, iv)); const ivBase64 = btoa(String.fromCharCode.apply(null, iv));
const response = await fetch(`${BASE_URL}/bin`, { const response = await fetch(`${origin}/bin`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -99,33 +99,18 @@ const Editor = () => {
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
navigator.clipboard const encryptedURL = `${origin}/${data.id}?key=${keyString}`;
.writeText(`${window.location.origin}/${data.id}?key=${keyString}`) copyToClipboard(encryptedURL);
.then( alert("URL copied to clipboard!");
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");
}
},
);
navigate(`/${data.id}?key=${keyString}`); navigate(`/${data.id}?key=${keyString}`);
} else { } else {
console.error(data); console.error(data);
} }
}; }, [text, language, navigate]);
const handleCancelClick = async () => { const handleCancelClick = useCallback(async () => {
setOpenModal(false); setOpenModal(false);
const response = await fetch(`${BASE_URL}/bin`, { const response = await fetch(`${origin}/bin`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -137,78 +122,72 @@ const Editor = () => {
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
navigator.clipboard const normalURL = `${origin}/${data.id}`;
.writeText(`${window.location.origin}/${data.id}`) copyToClipboard(normalURL);
.then( alert("URL copied to clipboard!");
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");
}
},
);
navigate(`/${data.id}`); navigate(`/${data.id}`);
} else { } else {
console.error(data); console.error(data);
} }
}; }, [text, language, navigate]);
const handleLanguageChange = (value) => { const handleLanguageChange = useCallback((value) => {
setLanguage(value); setLanguage(value);
}; }, []);
useEffect(() => { useEffect(() => {
Prism.highlightAll(); Prism.highlightAll();
}, [text, language]); }, [text, language]);
useEffect(() => { const fetchData = useCallback(async () => {
const fetchData = async () => { const response = await fetch(`${origin}/bin/${id}`);
const response = await fetch(`${BASE_URL}/bin/${id}`); const data = await response.json();
const data = await response.json(); if (response.ok) {
if (response.ok) { if (data.iv) {
if (data.iv) { const keyString = queryParams.get("key");
const keyString = queryParams.get("key"); const key = await stringToKey(keyString);
const key = await stringToKey(keyString); const encrypted = new Uint8Array(
const encrypted = new Uint8Array( atob(data.content)
atob(data.content) .split("")
.split("") .map((char) => char.charCodeAt(0)),
.map((char) => char.charCodeAt(0)), ).buffer;
).buffer; const ivArray = new Uint8Array(
const ivArray = new Uint8Array( atob(data.iv)
atob(data.iv) .split("")
.split("") .map((char) => char.charCodeAt(0)),
.map((char) => char.charCodeAt(0)), );
); const decryptedContent = await decryptAES(encrypted, key, ivArray);
const decryptedContent = await decryptAES(encrypted, key, ivArray); setLanguage(data.language);
setLanguage(data.language); setText(decryptedContent);
setText(decryptedContent); } else {
const isURL = URL_REGEX.test(data.content);
if (isURL) {
setText(`Your shortened URL: ${origin}/r/${id}`);
} else { } else {
const isURL = URL_REGEX.test(data.content); setLanguage(data.language);
if (isURL) { setText(data.content);
setText(`Your shortened URL: ${window.location.origin}/r/${id}`);
} else {
setLanguage(data.language);
setText(data.content);
}
} }
} }
}; }
}, [id, queryParams]);
useEffect(() => {
if (id) { if (id) {
fetchData(); fetchData();
} else { } else {
textareaRef.current.value = "";
setText(""); 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 ( return (
<> <>
@ -216,22 +195,17 @@ const Editor = () => {
<Modal <Modal
openModal={openModal} openModal={openModal}
setOpenModal={setOpenModal} setOpenModal={setOpenModal}
onSuccessClick={() => { onSuccessClick={handleSuccessClick}
handleSuccessClick(); onCancelClick={handleCancelClick}
}}
onCancelClick={() => {
handleCancelClick();
}}
/> />
<div className={styles.container}> <div className={styles.container}>
{!id && ( {!id && (
<button <button className={styles.btn__save} onClick={handleSaveClick}>
className={styles.btn__save} <img
onClick={() => { src="assets/icons/save.svg"
handleSaveClick(); className={styles.btn__icon}
}} alt="Save"
> />
<img src="assets/icons/save.svg" className={styles.btn__icon} />
</button> </button>
)} )}
@ -245,8 +219,9 @@ const Editor = () => {
spellCheck="false" spellCheck="false"
ref={textareaRef} ref={textareaRef}
placeholder="</> Paste, save, share! (Pasting just a URL will shorten it!)" placeholder="</> Paste, save, share! (Pasting just a URL will shorten it!)"
value={text}
/> />
<pre className="line-numbers"> <pre className="line-numbers" ref={lineNumberRef}>
<code <code
className={`${styles.codespace__code} language-${language}`} className={`${styles.codespace__code} language-${language}`}
> >
@ -261,4 +236,3 @@ const Editor = () => {
}; };
export default Editor; export default Editor;

View file

@ -1,4 +1,3 @@
export const BASE_URL = window.location.origin;
export const URL_REGEX = /^(https?:\/\/)?([\w.-]+\.[a-z]{2,})(\/?[^\s]*)?$/; export const URL_REGEX = /^(https?:\/\/)?([\w.-]+\.[a-z]{2,})(\/?[^\s]*)?$/;
export const SUPPORTED_LANGUAGES = [ export const SUPPORTED_LANGUAGES = [
{ {

View file

@ -1,26 +1,26 @@
async function generateAESKey() { async function generateAESKey() {
try { try {
const key = await window.crypto.subtle.generateKey( const key = await window.crypto.subtle.generateKey(
{ { name: "AES-GCM", length: 256 },
name: "AES-GCM",
length: 256,
},
true, true,
["encrypt", "decrypt"], ["encrypt", "decrypt"],
); );
return key; return key;
} catch (error) { } catch (error) {
console.error("Error generating AES key:", error); console.error("Error generating AES key:", error);
throw error;
} }
} }
async function keyToString(key) { async function keyToString(key) {
try { try {
const exportedKey = await window.crypto.subtle.exportKey("raw", key); const exportedKey = await window.crypto.subtle.exportKey("raw", key);
const keyString = btoa(String.fromCharCode(...new Uint8Array(exportedKey))); return btoa(String.fromCharCode(...new Uint8Array(exportedKey)))
return keyString.replace(/\+/g, "-").replace(/\//g, "_"); .replace(/\+/g, "-")
.replace(/\//g, "_");
} catch (error) { } catch (error) {
console.error("Error converting key to string:", error); console.error("Error converting key to string:", error);
throw error;
} }
} }
@ -31,17 +31,16 @@ async function stringToKey(keyString) {
c.charCodeAt(0), c.charCodeAt(0),
).buffer; ).buffer;
const key = await window.crypto.subtle.importKey( return await window.crypto.subtle.importKey(
"raw", "raw",
buffer, buffer,
{ name: "AES-GCM", length: 256 }, { name: "AES-GCM", length: 256 },
true, true,
["encrypt", "decrypt"], ["encrypt", "decrypt"],
); );
return key;
} catch (error) { } catch (error) {
console.error("Error converting string to key:", error); console.error("Error converting string to key:", error);
throw error;
} }
} }
@ -49,32 +48,28 @@ async function encryptAES(plaintext, key) {
try { try {
const iv = window.crypto.getRandomValues(new Uint8Array(12)); const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt( const encrypted = await window.crypto.subtle.encrypt(
{ { name: "AES-GCM", iv: iv },
name: "AES-GCM",
iv: iv,
},
key, key,
new TextEncoder().encode(plaintext), new TextEncoder().encode(plaintext),
); );
return { encrypted, iv }; return { encrypted, iv };
} catch (error) { } catch (error) {
console.error("Error encrypting:", error); console.error("Error encrypting:", error);
throw error;
} }
} }
async function decryptAES(encrypted, key, iv) { async function decryptAES(encrypted, key, iv) {
try { try {
const decrypted = await window.crypto.subtle.decrypt( const decrypted = await window.crypto.subtle.decrypt(
{ { name: "AES-GCM", iv: iv },
name: "AES-GCM",
iv: iv,
},
key, key,
encrypted, encrypted,
); );
return new TextDecoder().decode(decrypted); return new TextDecoder().decode(decrypted);
} catch (error) { } catch (error) {
console.error("Error decrypting:", error); console.error("Error decrypting:", error);
throw error;
} }
} }

View file

@ -7,37 +7,36 @@ import (
"log" "log"
"math/rand" "math/rand"
"net/http" "net/http"
"time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
var db *sql.DB var (
var dbFilePath string db *sql.DB
var port string dbFilePath string
port string
type Bin struct {
Content string `json:"content"`
Language string `json:"language"`
IV string `json:"iv"`
}
const (
shortIDCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" shortIDCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
shortIDLength = 8 shortIDLength = 8
) )
var ( type Bin struct {
//go:embed all:dist Content string `json:"content"`
dist embed.FS 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) { func RegisterHandlers(e *echo.Echo) {
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: nil,
Root: "dist", Root: "dist",
Index: "index.html", Index: "index.html",
HTML5: true, HTML5: true,
@ -55,6 +54,7 @@ func main() {
flag.Parse() flag.Parse()
initDatabase() initDatabase()
e := echo.New() e := echo.New()
RegisterHandlers(e) RegisterHandlers(e)
e.Logger.Fatal(e.Start(":" + port)) e.Logger.Fatal(e.Start(":" + port))
@ -67,47 +67,43 @@ func initDatabase() {
log.Fatal(err) log.Fatal(err)
} }
err = createTable() if err := createTable(); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func postBin(echoContext echo.Context) error { func postBin(c echo.Context) error {
bin := Bin{} var bin Bin
err := echoContext.Bind(&bin) if err := c.Bind(&bin); err != nil {
if err != nil {
return err return err
} }
id := generateShortID() id := generateShortID()
err = saveBin(id, bin) if err := saveBin(id, bin); err != nil {
if err != nil {
return err 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 { func getBin(c echo.Context) error {
id := echoContext.Param("id") id := c.Param("id")
bin, err := getBinById(id) bin, err := getBinByID(id)
if err != nil { if err != nil {
return err return err
} }
return echoContext.JSON(http.StatusOK, bin) return c.JSON(http.StatusOK, bin)
} }
func redirectToURL(echoContext echo.Context) error { func redirectToURL(c echo.Context) error {
id := echoContext.Param("id") id := c.Param("id")
bin, err := getBinById(id) bin, err := getBinByID(id)
if err != nil { if err != nil {
echoContext.Logger().Error(err) c.Logger().Error(err)
return err return err
} }
url := bin.Content return c.Redirect(http.StatusFound, bin.Content)
return echoContext.Redirect(http.StatusFound, url)
} }
func createTable() error { func createTable() error {
@ -115,20 +111,19 @@ func createTable() error {
return err return err
} }
func getBinById(id string) (Bin, error) { func getBinByID(id string) (Bin, error) {
row := db.QueryRow("SELECT content, language, iv FROM bins WHERE id = ?", id) var bin Bin
bin := Bin{} row := db.QueryRow(selectQuery, id)
err := row.Scan(&bin.Content, &bin.Language, &bin.IV) err := row.Scan(&bin.Content, &bin.Language, &bin.IV)
return bin, err return bin, err
} }
func saveBin(id string, bin Bin) error { 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 return err
} }
func generateShortID() string { func generateShortID() string {
rand.Seed(time.Now().UnixNano())
id := make([]byte, shortIDLength) id := make([]byte, shortIDLength)
for i := range id { for i := range id {
id[i] = shortIDCharset[rand.Intn(len(shortIDCharset))] id[i] = shortIDCharset[rand.Intn(len(shortIDCharset))]