Skip to content

Commit b14d00e

Browse files
authored
refactor(word-editor): word submit handling (#53)
This Pull Request refactors the word editor submit handling logic introduced in #32 and #39 - specifically the `handleSubmitWord` handler; This logic requires some side operations that trades off security for the purpose of just running at all. This changes transfers the operation performed by the handler to a new api route where they can be perform securely. ### Changes Made - Implemented the `/api/dictionary` route with a `POST` handler - this route take all the extracted logic from `handleSubmitWord` handler and does the following as side operation to perform the required operation - Retrieve the user `accessToken` from the HTTP `cookie` - Verifies the validity of this `accessToken` using the octokit instance of our OAuth App (finally a good use for the app 😁) - Retrieve the `title`, `content`, `action` and `metadata` value from the requests `formData` - Instantiates the `userOctokit` using the `accessToken` retrieved from cookies (obviously it's perceived as valid at this point) - Gets the jargons.dev app octokit instance as well - Then runs the old `handleSubmitWord` handler logic with all these available resource - Deleted `handleSubmitWord` handler extracting all of it logic to the `/api/dictionary` route - Deleted the `doOctokitAuth` action - it used to facilitate getting accessToken required in the `handleSubmitWord` handler, now we no longer need it - Replaced integration of `handleSubmitWord` with `/api/dictionary` API route in the `word-editor` Island doing the following to fulfil that - Added 2 new form fields of input type `hidden` - `action` - field to carry the current word-editor action which is either `new` or `edit` - `metadata` - a field that is only render when `action` is `edit`, it carries stringified object of required data for an already existing word that an edit is going to be performed on. - Implemented a base handler function local to the `Editor` component which integrates the `/api/dictionary` route ```js async function handleSubmit(e) { $isWordSubmitLoading.set(true); const formData = new FormData(e.target); const response = await fetch("/api/dictionary", { method: "POST", body: formData, }); response.status === 200 && router.push("/editor"); } ``` - Removed all instance of `octokitAuth` and `submitHandler` props - they used to be for the removed handler ### Screencast https://github.com/babblebey/jargons.dev/assets/25631971/e69c14ba-a480-4001-89af-6798821be323 📖
1 parent 0eeb366 commit b14d00e

7 files changed

Lines changed: 158 additions & 140 deletions

File tree

src/components/islands/word-editor.jsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,18 @@ import { useEffect } from "react";
22
import Markdown from "react-markdown";
33
import { useStore } from "@nanostores/react";
44
import useRouter from "../../lib/hooks/use-router.js";
5+
import { capitalizeText } from "../../lib/utils/index.js";
56
import useWordEditor from "../../lib/hooks/use-word-editor.js";
67
import { $isWordSubmitLoading } from "../../lib/stores/dictionary.js";
7-
import handleSubmitWord from "../../lib/handlers/handle-submit-word.js";
8-
import { capitalizeText } from "../../lib/utils/index.js";
98

10-
export default function WordEditor({ title = "", content = "", metadata = {}, action, octokitAuths }) {
9+
export default function WordEditor({ title = "", content = "", metadata = {}, action }) {
1110
return (
1211
<div className="w-full flex border rounded-lg">
1312
<Editor
1413
action={action}
1514
eTitle={title}
1615
eContent={content}
1716
eMetadata={metadata}
18-
octokitAuths={octokitAuths}
19-
submitHandler={handleSubmitWord}
2017
className="w-full h-full flex flex-col p-5 border-r"
2118
/>
2219
<Preview className="w-full h-full flex flex-col p-5" />
@@ -42,7 +39,7 @@ export function SubmitButton({ children = "Submit" }) {
4239
);
4340
}
4441

45-
function Editor({ eTitle, eContent, eMetadata, className, submitHandler, action, octokitAuths, ...props }) {
42+
function Editor({ eTitle, eContent, eMetadata, className, action, ...props }) {
4643
const router = useRouter();
4744
const { title, setTitle, content, setContent } = useWordEditor();
4845

@@ -51,39 +48,66 @@ function Editor({ eTitle, eContent, eMetadata, className, submitHandler, action,
5148
setContent(eContent);
5249
}, []);
5350

54-
async function handleOnSubmit() {
51+
/**
52+
* Word Submit Handler
53+
* @param {import("react").FormEvent} e
54+
*
55+
* @todo implement a submitted State that updates after submission for visual cue before routing
56+
* @todo handle error for when submission isn't successful
57+
*/
58+
async function handleSubmit(e) {
5559
$isWordSubmitLoading.set(true);
56-
await submitHandler(octokitAuths, action, {
57-
title: capitalizeText(title.trim()),
58-
content,
59-
metadata: eMetadata
60+
const formData = new FormData(e.target);
61+
const response = await fetch("/api/dictionary", {
62+
method: "POST",
63+
body: formData,
6064
});
61-
router.push("/editor");
65+
response.status === 200 && router.push("/editor");
6266
}
6367

6468
return (
6569
<form
6670
className={`${className} relative`}
6771
onSubmit={(e) => {
6872
e.preventDefault();
69-
handleOnSubmit();
73+
handleSubmit(e);
7074
}}
7175
id="jargons.dev:word_editor"
7276
{...props}
7377
>
7478
<input
75-
className={`${action === "edit" && "cursor-not-allowed"} block w-full pb-2 mb-3 text-gray-900 border-b text-lg font-bold focus:outline-none`}
79+
required
7680
type="text"
77-
placeholder="New Word"
81+
id="title"
82+
name="title"
7883
value={title}
84+
placeholder="New Word"
7985
readOnly={action === "edit"}
8086
onChange={(e) => setTitle(e.target.value)}
87+
className={`${action === "edit" && "cursor-not-allowed"} block w-full pb-2 mb-3 text-gray-900 border-b text-lg font-bold focus:outline-none`}
8188
/>
8289
<textarea
83-
className="w-full h-1 grow resize-none appearance-none border-none focus:outline-none scrollbar"
90+
required
91+
id="content"
92+
name="content"
8493
value={content}
8594
onChange={(e) => setContent(e.target.value)}
95+
className="w-full h-1 grow resize-none appearance-none border-none focus:outline-none scrollbar"
96+
/>
97+
<input
98+
type="hidden"
99+
id="action"
100+
name="action"
101+
value={action}
86102
/>
103+
{action === "edit" && (
104+
<input
105+
type="hidden"
106+
id="metadata"
107+
name="metadata"
108+
value={JSON.stringify(eMetadata)}
109+
/>
110+
)}
87111
</form>
88112
);
89113
}

src/lib/actions/do-octokit-auth.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/lib/handlers/handle-submit-word.js

Lines changed: 0 additions & 92 deletions
This file was deleted.

src/lib/utils/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,14 @@ export function resolveEditorActionFromPathname(pathname) {
5757
*/
5858
export function capitalizeText(text) {
5959
return text.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
60+
}
61+
62+
/**
63+
* Generate branch name
64+
* @param {string} action
65+
* @param {string} wordTitle
66+
* @returns {string}
67+
*/
68+
export function generateBranchName(action, wordTitle) {
69+
return `word/${action}/${normalizeAsUrl(wordTitle)}`;
6070
}

src/pages/api/dictionary.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import app from "../../lib/octokit/app.js";
2+
import { forkRepository } from "../../lib/fork.js";
3+
import { createBranch } from "../../lib/branch.js";
4+
import { decrypt } from "../../lib/utils/crypto.js";
5+
import { submitWord } from "../../lib/submit-word.js";
6+
import { PROJECT_REPO_DETAILS } from "../../../constants.js";
7+
import { updateExistingWord, writeNewWord } from "../../lib/word-editor.js";
8+
import { capitalizeText, generateBranchName } from "../../lib/utils/index.js";
9+
10+
/**
11+
* Submit Word (New or Edit) to the Dictionary
12+
* @param {import("astro").APIContext} context
13+
*/
14+
export async function POST({ request, cookies }) {
15+
const data = await request.formData();
16+
const accessToken = cookies.get("jargons.dev:token", {
17+
decode: value => decrypt(value)
18+
});
19+
20+
// Verify accessToken validity
21+
const { data: authData, status: verificationStatus } = await app.octokit.request("POST /applications/{client_id}/token", {
22+
client_id: import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID,
23+
access_token: accessToken.value
24+
});
25+
26+
if (!accessToken || verificationStatus !== 200) {
27+
return new Response(JSON.stringify({ message: "Not Authorised" }), {
28+
status: 401,
29+
headers: {
30+
"Content-type": "application/json"
31+
}
32+
})
33+
}
34+
35+
const title = capitalizeText(data.get("title").trim());
36+
const content = data.get("content");
37+
const action = data.get("action");
38+
const metadata = JSON.parse(data.get("metadata"));
39+
40+
const userOctokit = app.getUserOctokit({ token: accessToken.value });
41+
const devJargonsOctokit = app.devJargonsOctokit;
42+
43+
// Fork repo
44+
const fork = await forkRepository(userOctokit, PROJECT_REPO_DETAILS);
45+
console.log("Project Fork: ", fork);
46+
47+
// Create a branch for action
48+
const branch = await createBranch(
49+
userOctokit,
50+
{
51+
repoFullname: fork,
52+
repoMainBranchRef: PROJECT_REPO_DETAILS.repoMainBranchRef
53+
},
54+
generateBranchName(action, title)
55+
);
56+
console.log("Branch Created: ", branch);
57+
58+
const forkedRepoDetails = {
59+
repoFullname: fork,
60+
repoChangeBranchRef: branch.ref
61+
}
62+
63+
// update existing word - if action is "edit"
64+
if (action === "edit") {
65+
const updatedWord = await updateExistingWord(userOctokit, forkedRepoDetails, {
66+
title,
67+
content,
68+
path: metadata.path,
69+
sha: metadata.sha
70+
}, {
71+
env: "node"
72+
});
73+
console.log("Word updated: ", updatedWord);
74+
}
75+
76+
// add new word - if action is "new"
77+
if (action === "new") {
78+
const newWord = await writeNewWord(userOctokit, forkedRepoDetails, {
79+
title,
80+
content
81+
}, {
82+
env: "node"
83+
});
84+
console.log("New word added: ", newWord);
85+
}
86+
87+
// submit the edit in new pr
88+
const wordSubmission = await submitWord(
89+
devJargonsOctokit,
90+
userOctokit,
91+
action,
92+
PROJECT_REPO_DETAILS,
93+
forkedRepoDetails,
94+
{
95+
title,
96+
content
97+
}
98+
);
99+
console.log("Word submitted: ", wordSubmission);
100+
101+
return new Response(JSON.stringify(wordSubmission), {
102+
status: 200,
103+
headers: {
104+
"Content-type": "application/json"
105+
}
106+
});
107+
}

src/pages/editor/edit/[word].astro

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import BaseLayout from "../../../layouts/base.astro";
44
import Navbar from "../../../components/navbar.astro";
55
import doAuth from "../../../lib/actions/do-auth.js";
66
import doEditWord from "../../../lib/actions/do-edit-word.js";
7-
import doOctokitAuth from "../../../lib/actions/do-octokit-auth.js";
87
import { resolveEditorActionFromPathname } from "../../../lib/utils/index.js";
98
import WordEditor, { SubmitButton as WordEditorSubmitButton } from "../../../components/islands/word-editor.jsx";
109
@@ -13,8 +12,6 @@ const { url: { pathname }, redirect } = Astro;
1312
const { isAuthed, authedData: userData } = await doAuth(Astro);
1413
if (!isAuthed) return redirect(`/login?return_to=${encodeURIComponent(pathname)}`);
1514
16-
const octokitAuths = doOctokitAuth(Astro);
17-
1815
const {content_decoded, ...otherWordData} = await doEditWord(Astro);
1916
const word = matter(content_decoded);
2017
const action = resolveEditorActionFromPathname(pathname);
@@ -38,7 +35,6 @@ const action = resolveEditorActionFromPathname(pathname);
3835
title={word.data.title}
3936
content={word.content}
4037
metadata={otherWordData}
41-
octokitAuths={octokitAuths}
4238
client:load
4339
/>
4440
</main>

0 commit comments

Comments
 (0)