Forráskód Böngészése

Finish user login & add basic post login

jerryliao 1 éve
szülő
commit
b84ac8990d

+ 48 - 0
deno.lock

@@ -77,7 +77,22 @@
     "https://deno.land/std@0.178.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
     "https://deno.land/std@0.178.0/semver/mod.ts": "409a2691f5a411c34e917c1e6d445a6d1d53f3fadf660e44a99dd0bf9b2ef412",
     "https://deno.land/std@0.184.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
+    "https://deno.land/std@0.184.0/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332",
+    "https://deno.land/std@0.184.0/crypto/_fnv/fnv32.ts": "e4649dfdefc5c987ed53c3c25db62db771a06d9d1b9c36d2b5cf0853b8e82153",
+    "https://deno.land/std@0.184.0/crypto/_fnv/fnv64.ts": "bfa0e4702061fdb490a14e6bf5f9168a22fb022b307c5723499469bfefca555e",
+    "https://deno.land/std@0.184.0/crypto/_fnv/mod.ts": "f956a95f58910f223e420340b7404702ecd429603acd4491fa77af84f746040c",
+    "https://deno.land/std@0.184.0/crypto/_fnv/util.ts": "accba12bfd80a352e32a872f87df2a195e75561f1b1304a4cb4f5a4648d288f9",
+    "https://deno.land/std@0.184.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "bdd70a6183c6bdabc086ec2a5f828c86711b4201f1ba7954fc78385a664e8fae",
+    "https://deno.land/std@0.184.0/crypto/_wasm/mod.ts": "e2df88236fc061eac7a89e8cb0b97843f5280b08b2a990e473b7397a3e566003",
+    "https://deno.land/std@0.184.0/crypto/crypto.ts": "051d941627f1b6da0d5151d744e6c94658cdf43bfe14621610f0d7268f52b05f",
+    "https://deno.land/std@0.184.0/crypto/keystack.ts": "877ab0f19eb7d37ad6495190d3c3e39f58e9c52e0b6a966f82fd6df67ca55f90",
+    "https://deno.land/std@0.184.0/crypto/mod.ts": "ae384519e85eca9aeff4e7111ed153df8f3dbda7b35b70850ed4b3e9c8cec4d5",
+    "https://deno.land/std@0.184.0/crypto/timing_safe_equal.ts": "0fae34ee02264f309ae0b6e54e9746a7aba3996e5454903ed106967a7a9ef665",
+    "https://deno.land/std@0.184.0/crypto/to_hash_string.ts": "6927c768f3e373a1be4a31555a45ccecf7bd413105455cc334ad3f908cfa986f",
     "https://deno.land/std@0.184.0/datetime/to_imf.ts": "8f9c0af8b167031ffe2e03da01a12a3b0672cc7562f89c61942a0ab0129771b2",
+    "https://deno.land/std@0.184.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d",
+    "https://deno.land/std@0.184.0/encoding/base64url.ts": "2ed4ba122b20fedf226c5d337cf22ee2024fa73a8f85d915d442af7e9ce1fae1",
+    "https://deno.land/std@0.184.0/encoding/hex.ts": "b4b1a7cb678745b0bf181ed8cf2498c7be00d121a7de244b752fbf9c7d9c48cd",
     "https://deno.land/std@0.184.0/http/cookie.ts": "934f92d871d50852dbd7a836d721df5a9527b14381db16001b40991d30174ee4",
     "https://deno.land/x/code_block_writer@11.0.3/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5",
     "https://deno.land/x/code_block_writer@11.0.3/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
@@ -128,13 +143,46 @@
     "https://deno.land/x/ts_morph@17.0.1/mod.ts": "adba9b82f24865d15d2c78ef6074b9a7457011719056c9928c800f130a617c93",
     "https://deno.land/x/ts_morph@17.0.1/ts_morph.d.ts": "a54b0c51b06d84defedf5fdd59c773d803808ae7c9678f7165f7a1a6dfa7f6a3",
     "https://deno.land/x/ts_morph@17.0.1/ts_morph.js": "1bb80284b9e31a4c5c2078cd533fe9b12b4b2d710267055cb655225aa88fb2df",
+    "https://esm.sh/lodash@4.17.21/debounce": "6e1de1c30f2e685be1082e4904c7ed95c10829b2e30c3a3e35fdb714ef816736",
+    "https://esm.sh/lodash@4.17.21/throttle": "996b17e983a8a862c5d1761d94b417436ae3cef82f7d7cedc54ae04aa929e2e9",
     "https://esm.sh/preact-render-to-string@6.0.2?deps=preact@10.13.2": "df36fa1d653fc00225ec484bee9d8075df042f21cb861b97d40b5aff9029a6c7",
     "https://esm.sh/preact@10.13.2": "5ebf0838bbc3c32fc6e78a4cd9dd672f2c36386d0595815d721a6b0941278488",
     "https://esm.sh/preact@10.13.2/hooks": "884334b1560448cf16b4f14841fffdb8707615373a3c76c676a6f9e5c77e43b2",
     "https://esm.sh/showdown@2.1.0": "8401aeb424b74dcdf53811256d1467343610fc05c0430bb2460d4a3c4d5d5e34",
     "https://esm.sh/stable/preact@10.13.2/deno/hooks.js": "c7a8e703bcbc6a05949f329b618c33d5d1ea5fee113ddcea44ff0f527af8556f",
     "https://esm.sh/stable/preact@10.13.2/deno/preact.mjs": "365fab897381f4f403f859c5d12939084560545567108cc90dae901bbe892578",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/array.d.ts": "458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/collection.d.ts": "d70c026dd2eeaa974f430ea229230a1897fdb897dc74659deebe2afd4feeb08f",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/common.d.ts": "675e702f2032766a91eeadee64f51014c64688525da99dccd8178f0c599f13a8",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/date.d.ts": "187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/function.d.ts": "febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/lang.d.ts": "98f9d826db9cd99d27a01a59ee5f22863df00ccf1aaf43e1d7db80ebf716f7c3",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/math.d.ts": "0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/number.d.ts": "00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/object.d.ts": "dcd91d3b697cb650b95db5471189b99815af5db2a1cd28760f91e0b12ede8ed5",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/seq.d.ts": "3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/string.d.ts": "3cf0d343c2276842a5b617f22ba82af6322c7cfe8bb52238ffc0c491a3c21019",
+    "https://esm.sh/v116/@types/lodash@4.14.194/common/util.d.ts": "df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9",
+    "https://esm.sh/v116/@types/lodash@4.14.194/debounce~.d.ts": "9905281a4c9cf23ee1a9d057d1f39001be920891aa1f70af1299c5d87ca415ed",
+    "https://esm.sh/v116/@types/lodash@4.14.194/index.d.ts": "f2eff8704452659641164876c1ef0df4174659ce7311b0665798ea3f556fa9ad",
+    "https://esm.sh/v116/@types/lodash@4.14.194/index~.d.ts": "f2eff8704452659641164876c1ef0df4174659ce7311b0665798ea3f556fa9ad",
+    "https://esm.sh/v116/@types/lodash@4.14.194/throttle~.d.ts": "45124b18f9b81a45134db1f87474642d73b2d107666dce00b6837a58dc2e48d4",
     "https://esm.sh/v116/@types/showdown@2.0.0/index.d.ts": "0cae38d42cef880ce4848fcd9ed704d65ba3ecc825443cd548ff81931c64cdca",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_Symbol.js": "1b59f6fb5e8a0581931ac79bc340db7e60141a3ed1ae57071489e4e050c3cd3c",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_baseGetTag.js": "e3153929bcf9bb84bcbf6e64c444f2dec4f184f4978aca556447c8cc853f4d5d",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_baseTrim.js": "5d2d3bd7093a9d683faa672a85ee2162e0dcacdb3292ff6f743d0d565954c866",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_freeGlobal.js": "447608867b27f11ab7f56689d66ae4039cad253a2a23cb938d9fcc9d76ae851a",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_getRawTag.js": "619a5eabaa02d9b6a0a1bf21479af52b7fccf79205df6691b4f563671b8a3916",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_objectToString.js": "24ff88d51d498df9bc088cfdb04d0c4c7c2994222d76cd8fb0ab44351850fa2b",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_root.js": "b9261ef2793dfc9b554f7792bce207b9ac96b51ebfa40c396554438a054f2cdb",
+    "https://esm.sh/v116/lodash@4.17.21/deno/_trimmedEndIndex.js": "6c1f7b5703754aa023a9073626e289f53997964f6b0b45a3e189526b4f0fa93a",
+    "https://esm.sh/v116/lodash@4.17.21/deno/debounce.js": "9b61d8f726b2edef89bd02661301de2497248e2e231c1d8700be1b5d7549aea6",
+    "https://esm.sh/v116/lodash@4.17.21/deno/isObject.js": "eb62b3ca7b47f1fa4e019c86abdb5583b6668c6f7f975f8b756f458eb4dfdea9",
+    "https://esm.sh/v116/lodash@4.17.21/deno/isObjectLike.js": "04ce534c0b87fa884d7f8c6dd0ac98f2f278c54a34ace14c399e00f64c902358",
+    "https://esm.sh/v116/lodash@4.17.21/deno/isSymbol.js": "7aa09c0401bc53361a2c1568192e5437088a850adca8437dfbd62981b4b61f2d",
+    "https://esm.sh/v116/lodash@4.17.21/deno/now.js": "09c5cfb11430a8c2a0abe287dccbbddf57a3f383b6f41dc6f909965d26a45730",
+    "https://esm.sh/v116/lodash@4.17.21/deno/throttle.js": "51b437e666781ec14642e4499532ef28c39e2f18422dcd66027b683b6ded0b5c",
+    "https://esm.sh/v116/lodash@4.17.21/deno/toNumber.js": "7ba1d7b56501ddd6fabb52c8411189a81762e3dc08e9c726ea6e363b6f969ed5",
     "https://esm.sh/v116/preact-render-to-string@6.0.2/X-ZC9wcmVhY3RAMTAuMTMuMg/deno/preact-render-to-string.mjs": "09ead691b3745189a4171a6ee0948592c7a862b1bf1a97fe651fb34f88eabe3b",
     "https://esm.sh/v116/preact-render-to-string@6.0.2/X-ZC9wcmVhY3RAMTAuMTMuMg/src/index.d.ts": "36e470f81eacf9700b80c92d7bf7342f0d46e2e7152a31cbe166576bb634a96e",
     "https://esm.sh/v116/preact@10.13.2/hooks/src/index.d.ts": "5c29febb624fc25d71cb0e125848c9b711e233337a08f7eacfade38fd4c14cc3",

+ 28 - 16
fresh.gen.ts

@@ -3,29 +3,41 @@
 // This file is automatically updated during development when running `dev.ts`.
 
 import config from "./deno.json" assert { type: "json" };
-import * as $0 from "./routes/_app.tsx";
-import * as $1 from "./routes/api/post.tsx";
-import * as $2 from "./routes/api/user/login.tsx";
-import * as $3 from "./routes/api/user/logout.tsx";
-import * as $4 from "./routes/index.tsx";
-import * as $5 from "./routes/login.tsx";
+import * as $0 from "./routes/_404.tsx";
+import * as $1 from "./routes/_app.tsx";
+import * as $2 from "./routes/api/post.tsx";
+import * as $3 from "./routes/api/user/login.tsx";
+import * as $4 from "./routes/api/user/logout.tsx";
+import * as $5 from "./routes/api/user/register.tsx";
+import * as $6 from "./routes/index.tsx";
+import * as $7 from "./routes/login.tsx";
+import * as $8 from "./routes/post/[id].tsx";
+import * as $9 from "./routes/register.tsx";
 import * as $$0 from "./islands/Editor.tsx";
-import * as $$1 from "./islands/LoginFrame.tsx";
-import * as $$2 from "./islands/TopBar.tsx";
+import * as $$1 from "./islands/HomeBar.tsx";
+import * as $$2 from "./islands/LoginFrame.tsx";
+import * as $$3 from "./islands/PostList.tsx";
+import * as $$4 from "./islands/TopBar.tsx";
 
 const manifest = {
   routes: {
-    "./routes/_app.tsx": $0,
-    "./routes/api/post.tsx": $1,
-    "./routes/api/user/login.tsx": $2,
-    "./routes/api/user/logout.tsx": $3,
-    "./routes/index.tsx": $4,
-    "./routes/login.tsx": $5,
+    "./routes/_404.tsx": $0,
+    "./routes/_app.tsx": $1,
+    "./routes/api/post.tsx": $2,
+    "./routes/api/user/login.tsx": $3,
+    "./routes/api/user/logout.tsx": $4,
+    "./routes/api/user/register.tsx": $5,
+    "./routes/index.tsx": $6,
+    "./routes/login.tsx": $7,
+    "./routes/post/[id].tsx": $8,
+    "./routes/register.tsx": $9,
   },
   islands: {
     "./islands/Editor.tsx": $$0,
-    "./islands/LoginFrame.tsx": $$1,
-    "./islands/TopBar.tsx": $$2,
+    "./islands/HomeBar.tsx": $$1,
+    "./islands/LoginFrame.tsx": $$2,
+    "./islands/PostList.tsx": $$3,
+    "./islands/TopBar.tsx": $$4,
   },
   baseUrl: import.meta.url,
   config,

+ 2 - 0
import_map.json

@@ -2,6 +2,8 @@
   "imports": {
     "$fresh/": "https://deno.land/x/fresh@1.1.5/",
     "$sqlite/": "https://deno.land/x/sqlite@v3.7.1/",
+    "$crypto/": "https://deno.land/std@0.184.0/crypto/",
+    "$async/": "https://deno.land/std@0.184.0/async/",
     "$http/": "https://deno.land/std@0.184.0/http/",
     "preact": "https://esm.sh/preact@10.13.2",
     "preact/": "https://esm.sh/preact@10.13.2/",

+ 79 - 31
islands/Editor.tsx

@@ -2,17 +2,28 @@
 import { h, render } from "preact";
 import { useEffect, useState, useRef } from "preact/hooks";
 import showdown, { Converter } from "showdown";
-import { showLoading, hideLoading } from "utils/ui.ts";
+import { debounce, DebouncedFunction } from "$async/debounce.ts";
+import { hideLoading } from "utils/ui.ts";
+
+export enum EditorMode {
+  Edit,
+  Read,
+  Both,
+}
 
 interface EditorProps {
-  id: string;
-  allowMode: "edit" | "read" | "both";
+  id: number;
+  title: string;
+  content: string;
+  allowMode: EditorMode;
 }
 
 let shadow: ShadowRoot | null = null;
 let shadowRoot: HTMLDivElement | null = null;
 let converter: Converter | null = null;
-let scrollingSide: "edit" | "read" | null = null;
+let scrollingSide: EditorMode | null = null;
+let debouncedOnSave: DebouncedFunction | null = null;
+
 export default function Editor(props: EditorProps) {
   const [mode, setMode] = useState(props.allowMode);
   const [prevMode, setPrevMode] = useState(props.allowMode);
@@ -23,7 +34,7 @@ export default function Editor(props: EditorProps) {
   const readViewRef = useRef(null);
   const editViewRef = useRef(null);
 
-  const checkSyncScroll = (scrollSide: "edit" | "read") => {
+  const checkSyncScroll = (scrollSide: EditorMode) => {
     if (scrollingSide && scrollingSide !== scrollSide) {
       scrollingSide = null;
       return false;
@@ -33,14 +44,14 @@ export default function Editor(props: EditorProps) {
   };
 
   // Sync scroll on both sides
-  const onScroll = (scrollSide: "edit" | "read") => {
+  const onScroll = (scrollSide: EditorMode) => {
     // Do not trigger sync on other side
     if (!checkSyncScroll(scrollSide)) {
       return;
     }
 
     const currentElement =
-      scrollSide === "read"
+      scrollSide === EditorMode.Read
         ? readViewRef.current
         : editViewRef.current &&
           (editViewRef.current as HTMLDivElement).querySelector("textarea");
@@ -53,7 +64,7 @@ export default function Editor(props: EditorProps) {
 
       // Sync scroll ratio
       const syncElement =
-        scrollSide === "read"
+        scrollSide === EditorMode.Edit
           ? editViewRef.current &&
             (editViewRef.current as HTMLDivElement).querySelector("textarea")
           : readViewRef.current;
@@ -66,6 +77,32 @@ export default function Editor(props: EditorProps) {
     }
   };
 
+  // Unload listener
+  const onUnload = (e: BeforeUnloadEvent) => {
+    e.preventDefault();
+    e.returnValue = "";
+    return false;
+  };
+
+  // Save changes
+  const onSave = async (content: string) => {
+    addEventListener("beforeunload", onUnload);
+
+    // Send request
+    await fetch("/api/post", {
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        id: props.id,
+        title: props.title,
+        content,
+      }),
+    });
+
+    // Remove listener
+    removeEventListener("beforeunload", onUnload);
+  };
+
   // Render converted content to shadow root
   const renderContentToShadow = () => {
     if (readViewRef && readViewRef.current) {
@@ -90,7 +127,7 @@ export default function Editor(props: EditorProps) {
   const modeChangeListener = (e: CustomEvent) => {
     if (
       e.detail &&
-      (props.allowMode === e.detail || props.allowMode === "both")
+      (props.allowMode === e.detail || props.allowMode === EditorMode.Both)
     ) {
       setMode(e.detail);
     }
@@ -98,7 +135,6 @@ export default function Editor(props: EditorProps) {
 
   // Init event listeners
   useEffect(() => {
-    showLoading();
     addEventListener("ModeChange", modeChangeListener);
 
     return () => {
@@ -110,7 +146,7 @@ export default function Editor(props: EditorProps) {
   // Note: cannot access latest state at global function
   useEffect(() => {
     // Sync scroll when switched to both mode
-    if (mode === "both" && prevMode !== "both") {
+    if (mode === EditorMode.Both && prevMode !== EditorMode.Both) {
       onScroll(prevMode);
     }
     setPrevMode(mode);
@@ -123,19 +159,12 @@ export default function Editor(props: EditorProps) {
 
   // Init conversion
   useEffect(() => {
-    const loadPost = async () => {
-      if (props.id) {
-        const resp = await fetch("/api/post");
-        const respJson = await resp.json();
-        if (respJson.success) {
-          setDisplayContent(respJson.data);
-          convertText(respJson.data);
-          hideLoading();
-        }
-      }
-    };
-    loadPost();
-  }, [props.id]);
+    if (props.title) {
+      setDisplayContent(props.content);
+      convertText(props.content);
+      hideLoading();
+    }
+  }, [props.title]);
 
   const convertText = (text: string) => {
     // Init converter
@@ -146,23 +175,42 @@ export default function Editor(props: EditorProps) {
     // Save display text
     setDisplayContent(text);
 
-    // Convert text and save
+    // Convert text
     setConvertedContent(converter.makeHtml(text));
+
+    // Trigger save
+    if (text !== props.content) {
+      if (!debouncedOnSave) {
+        debouncedOnSave = debounce(onSave, 2000);
+      }
+      debouncedOnSave(text);
+    }
+  };
+
+  const getModeText = (mode: EditorMode) => {
+    switch (mode) {
+      case EditorMode.Read:
+        return "read";
+      case EditorMode.Edit:
+        return "edit";
+      case EditorMode.Both:
+        return "both";
+    }
   };
 
   return (
-    <div className={`pd-editor pd-mode-${mode}`}>
-      {props.allowMode !== "read" ? (
+    <div className={`pd-editor pd-mode-${getModeText(mode)}`}>
+      {props.allowMode !== EditorMode.Read ? (
         <div className="pd-edit-view" ref={editViewRef}>
           <textarea
             placeholder="Some Markdown here"
             onScroll={() => {
-              onScroll("edit");
+              onScroll(EditorMode.Edit);
             }}
             onPaste={() => {
               // Sync scroll again after render
               setTimeout(() => {
-                onScroll("edit");
+                onScroll(EditorMode.Edit);
               }, 100);
             }}
             onInput={(e) => {
@@ -172,12 +220,12 @@ export default function Editor(props: EditorProps) {
           />
         </div>
       ) : null}
-      {props.allowMode !== "edit" ? (
+      {props.allowMode !== EditorMode.Edit ? (
         <div
           className="pd-read-view"
           ref={readViewRef}
           onScroll={() => {
-            onScroll("read");
+            onScroll(EditorMode.Read);
           }}
         />
       ) : null}

+ 47 - 0
islands/HomeBar.tsx

@@ -0,0 +1,47 @@
+/** @jsx h */
+import { h } from "preact";
+import { showLoading } from "utils/ui.ts";
+
+interface HomeBarProps {
+  name: string;
+}
+
+export default function HomeBar(props: HomeBarProps) {
+  const doNewPost = async () => {
+    showLoading();
+    const resp = await fetch("/api/post", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        title: "",
+        content: "",
+      }),
+    });
+    const respJson = await resp.json();
+    if (respJson.success) {
+      location.href = `/post/${respJson.data}`;
+      return true;
+    }
+    return false;
+  };
+
+  const doLogout = async () => {
+    const resp = await fetch("/api/user/logout");
+    const respJson = await resp.json();
+    if (respJson.success) {
+      location.href = "/login";
+      return true;
+    }
+    return false;
+  };
+
+  return (
+    <div className="pd-home-bar">
+      <button onClick={doNewPost}>New Post</button>
+      <div className="pd-home-user-info">
+        <span>{props.name}</span>
+        <button onClick={doLogout}>Logout</button>
+      </div>
+    </div>
+  );
+}

+ 60 - 5
islands/LoginFrame.tsx

@@ -1,13 +1,20 @@
 /** @jsx h */
-import { h } from "preact";
+/** @jsxFrag Fragment */
+import { Fragment, h } from "preact";
 import { useState, useEffect } from "preact/hooks";
 import { showLoading, hideLoading } from "utils/ui.ts";
 
-export default function LoginFrame() {
+interface LoginFrameProps {
+  mode: "login" | "register";
+}
+
+export default function LoginFrame(props: LoginFrameProps) {
   const [email, setEmail] = useState("");
   const [password, setPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
   const [emailError, setEmailError] = useState(false);
   const [passwordError, setPasswordError] = useState(false);
+  const [confirmPasswordError, setConfirmPasswordError] = useState(false);
 
   const checkUserLogin = async () => {
     const resp = await fetch("/api/user/login");
@@ -37,14 +44,44 @@ export default function LoginFrame() {
     return false;
   };
 
+  const doUserRegister = async () => {
+    const resp = await fetch("/api/user/register", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        email,
+        password,
+      }),
+    });
+    const respJson = await resp.json();
+    if (respJson.success) {
+      location.href = "/login";
+      return true;
+    }
+    return false;
+  };
+
   useEffect(() => {
-    checkUserLogin();
+    props.mode === "login" && checkUserLogin();
   }, []);
 
   const onSubmit = async () => {
     showLoading();
-    if (email && password) {
+
+    // Do request
+    if (email && password && props.mode === "login") {
       await doUserLogin();
+    } else if (
+      email &&
+      password &&
+      confirmPassword &&
+      props.mode === "register"
+    ) {
+      if (password !== confirmPassword) {
+        setConfirmPasswordError(true);
+      } else {
+        await doUserRegister();
+      }
     }
 
     // Set error
@@ -54,6 +91,9 @@ export default function LoginFrame() {
     if (!password) {
       setPasswordError(true);
     }
+    if (!confirmPassword && props.mode === "register") {
+      setConfirmPasswordError(true);
+    }
     hideLoading();
   };
 
@@ -81,8 +121,23 @@ export default function LoginFrame() {
           setPassword((e.target as HTMLInputElement).value);
         }}
       />
+      {props.mode === "register" ? (
+        <>
+          <span className="pd-login-input-label">Confirm Password</span>
+          <input
+            className={`pd-login-input${confirmPasswordError ? " error" : ""}`}
+            type="password"
+            placeholder="Confirm your password"
+            value={confirmPassword}
+            onInput={(e) => {
+              setConfirmPasswordError(false);
+              setConfirmPassword((e.target as HTMLInputElement).value);
+            }}
+          />
+        </>
+      ) : null}
       <button className="pd-login-btn" type="button" onClick={onSubmit}>
-        Sign in
+        {props.mode === "register" ? "Register" : "Sign in"}
       </button>
     </div>
   );

+ 30 - 0
islands/PostList.tsx

@@ -0,0 +1,30 @@
+/** @jsx h */
+import { h } from "preact";
+
+interface PostListProps {
+  posts: { id: number; title: string; content: string; shared: boolean }[];
+}
+
+export default function PostList(props: PostListProps) {
+  const onEdit = (id: number) => {
+    location.href = `post/${id}`;
+  };
+
+  return (
+    <div className="pd-post-list">
+      {props.posts.map((post) => (
+        <div className="pd-post" key={post.id}>
+          <span className="pd-post-title">{post.title || "Untitled"}</span>
+          <span className="pd-post-digest">{post.content || "No content"}</span>
+          <button
+            onClick={() => {
+              onEdit(post.id);
+            }}
+          >
+            Edit
+          </button>
+        </div>
+      ))}
+    </div>
+  );
+}

+ 23 - 20
islands/TopBar.tsx

@@ -1,20 +1,21 @@
 /** @jsx h */
 import { h } from "preact";
 import { useEffect, useState } from "preact/hooks";
+import { EditorMode } from "./Editor.tsx";
 
 interface TopBarProps {
-  allowMode: "edit" | "read" | "both";
+  allowMode: EditorMode;
+  isLogined: boolean;
 }
 
 export default function TopBar(props: TopBarProps) {
   const [mode, setMode] = useState(props.allowMode);
-  const [isLogin, setIsLogin] = useState(false);
 
   // Event listener
   const modeChangeListener = (e: CustomEvent) => {
     if (
       e.detail &&
-      (props.allowMode === e.detail || props.allowMode === "both")
+      (props.allowMode === e.detail || props.allowMode === EditorMode.Both)
     ) {
       setMode(e.detail);
     }
@@ -25,21 +26,22 @@ export default function TopBar(props: TopBarProps) {
     dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
   };
 
-  const checkUserLogin = async () => {
-    const resp = await fetch("/api/user/login");
+  const doLogout = async () => {
+    const resp = await fetch("/api/user/logout");
     const respJson = await resp.json();
     if (respJson.success) {
-      setIsLogin(true);
+      location.href = "/login";
       return true;
     }
-    // Redirect to login page if not valid
-    location.href = "/login";
     return false;
   };
 
+  const goHome = () => {
+    location.href = "/";
+  };
+
   // Init event listeners
   useEffect(() => {
-    checkUserLogin();
     addEventListener("ModeChange", modeChangeListener);
 
     return () => {
@@ -51,9 +53,9 @@ export default function TopBar(props: TopBarProps) {
     <div className="pd-top-bar">
       <div className="pd-top-bar-mode-switcher">
         <button
-          className={`pd-top-bar-btn${mode === "edit" ? " active" : ""}${
-            props.allowMode === "read" ? " disabled" : ""
-          }`}
+          className={`pd-top-bar-btn${
+            mode === EditorMode.Edit ? " active" : ""
+          }${props.allowMode === EditorMode.Read ? " disabled" : ""}`}
           id="edit"
           type="button"
           onClick={() => {
@@ -63,9 +65,9 @@ export default function TopBar(props: TopBarProps) {
           Edit
         </button>
         <button
-          className={`pd-top-bar-btn${mode === "read" ? " active" : ""}${
-            props.allowMode === "edit" ? " disabled" : ""
-          }`}
+          className={`pd-top-bar-btn${
+            mode === EditorMode.Read ? " active" : ""
+          }${props.allowMode === EditorMode.Edit ? " disabled" : ""}`}
           id="read"
           type="button"
           onClick={() => {
@@ -75,9 +77,9 @@ export default function TopBar(props: TopBarProps) {
           Read
         </button>
         <button
-          className={`pd-top-bar-btn${mode === "both" ? " active" : ""}${
-            props.allowMode !== "both" ? " disabled" : ""
-          }`}
+          className={`pd-top-bar-btn${
+            mode === EditorMode.Both ? " active" : ""
+          }${props.allowMode !== EditorMode.Both ? " disabled" : ""}`}
           id="both"
           type="button"
           onClick={() => {
@@ -87,9 +89,10 @@ export default function TopBar(props: TopBarProps) {
           Both
         </button>
       </div>
-      {isLogin ? (
+      {props.isLogined ? (
         <div className="pd-top-bar-tool-icons">
-          <i className="bi bi-house-door" />
+          <i className="bi bi-box-arrow-left" onClick={doLogout} />
+          <i className="bi bi-house-door" onClick={goHome} />
           <i className="bi bi-share" />
           <i className="bi bi-gear" />
         </div>

+ 11 - 0
routes/_404.tsx

@@ -0,0 +1,11 @@
+/** @jsx h */
+import { h } from "preact";
+import { UnknownPageProps } from "$fresh/server.ts";
+
+export default function NotFound(props: UnknownPageProps) {
+  return (
+    <div className="pd-page pd-page-centered">
+      Not Found: {props.url.pathname}
+    </div>
+  );
+}

+ 2 - 2
routes/_app.tsx

@@ -4,7 +4,7 @@ import { Fragment, h } from "preact";
 import { asset, Head } from "$fresh/runtime.ts";
 import { AppProps } from "$fresh/server.ts";
 
-export default function App(props: AppProps) {
+export default function App({ Component }: AppProps) {
   return (
     <>
       <Head>
@@ -14,7 +14,7 @@ export default function App(props: AppProps) {
           rel="stylesheet"
         />
       </Head>
-      <props.Component />
+      <Component />
     </>
   );
 }

+ 78 - 6
routes/api/post.tsx

@@ -4,16 +4,88 @@ import {
   makeErrorResponse,
   makeSuccessResponse,
 } from "utils/server.ts";
+import { del, find, insert, update } from "utils/db.ts";
 
 export const handler: Handlers = {
   async GET(req: Request) {
-    // Mock post content
-    if (checkToken(req)) {
-      const resp = await fetch(
-        "https://raw.githubusercontent.com/denoland/deno/main/README.md"
+    const reqJson = await req.json();
+    const id = reqJson.id;
+    const tokenUserId = checkToken(req);
+    if (id) {
+      const post = find(
+        "Post",
+        tokenUserId
+          ? {
+              id,
+              user_id: tokenUserId,
+            }
+          : {
+              id,
+              shared: true,
+            },
+        ["title", "content"]
       );
-      if (resp.status === 200) {
-        return makeSuccessResponse(await resp.text());
+      if (post.length > 0) {
+        return makeSuccessResponse({
+          title: post[0][0] as string,
+          content: post[0][1] as string,
+        });
+      }
+    }
+    return makeErrorResponse();
+  },
+  async POST(req: Request) {
+    const reqJson = await req.json();
+    const title = reqJson.title || "";
+    const content = reqJson.content || "";
+    const tokenUserId = checkToken(req);
+    if (tokenUserId) {
+      const post = insert("Post", {
+        title,
+        content,
+        user_id: tokenUserId,
+        shared: false,
+      });
+      if (post.length > 0) {
+        return makeSuccessResponse(post[0][0] as string);
+      }
+    }
+    return makeErrorResponse();
+  },
+  async PUT(req: Request) {
+    const reqJson = await req.json();
+    const id = reqJson.id;
+    const title = reqJson.title;
+    const content = reqJson.content;
+    const tokenUserId = checkToken(req);
+    if (tokenUserId && id && title && content) {
+      const post = find("Post", {
+        id,
+        user_id: tokenUserId,
+      });
+      if (post.length > 0) {
+        const newPost = update("Post", id, {
+          title,
+          content,
+        });
+        if (newPost.length > 0) {
+          return makeSuccessResponse(true);
+        }
+      }
+    }
+    return makeErrorResponse();
+  },
+  async DELETE(req: Request) {
+    const reqJson = await req.json();
+    const id = reqJson.id;
+    const tokenUserId = checkToken(req);
+    if (tokenUserId && id) {
+      const post = del("Post", {
+        id,
+        user_id: tokenUserId,
+      });
+      if (post.length > 0) {
+        return makeSuccessResponse(true);
       }
     }
     return makeErrorResponse();

+ 42 - 12
routes/api/user/login.tsx

@@ -1,30 +1,60 @@
 import { Handlers } from "$fresh/server.ts";
 import {
+  setToken,
   checkToken,
   makeErrorResponse,
   makeSuccessResponse,
-  setToken,
+  getCryptoString,
 } from "utils/server.ts";
-import { find } from "utils/db.ts";
+import { find, insert } from "utils/db.ts";
 
 export const handler: Handlers = {
   GET(req: Request) {
-    // Mock a default user
-    if (checkToken(req)) {
-      return makeSuccessResponse({
-        name: "Jerry Liao",
-        email: "jerryliao26@gmail.com",
-      });
+    const tokenUserId = checkToken(req);
+    if (tokenUserId) {
+      const user = find(
+        "User",
+        {
+          id: tokenUserId,
+        },
+        ["name", "email"]
+      );
+      if (user.length > 0) {
+        return makeSuccessResponse({
+          name: user[0][0] as string,
+          email: user[0][1] as string,
+        });
+      }
     }
     return makeErrorResponse();
   },
   async POST(req: Request) {
     const reqJson = await req.json();
     if (reqJson.email && reqJson.password) {
-      if (find("User", { email: reqJson.email, password: reqJson.password })) {
-        const successResponse = makeSuccessResponse(true);
-        setToken(successResponse);
-        return successResponse;
+      const user = find(
+        "User",
+        {
+          email: reqJson.email,
+        },
+        ["id"]
+      );
+      if (user.length > 0) {
+        // Generate token
+        const token = await getCryptoString(
+          reqJson.email + new Date().toString(),
+          "MD5"
+        );
+
+        // Store token
+        const newToken = insert("Token", {
+          token,
+          user_id: user[0][0] as string,
+        });
+        if (newToken.length > 0) {
+          const successResponse = makeSuccessResponse(true);
+          setToken(successResponse, token);
+          return successResponse;
+        }
       }
     }
     return makeErrorResponse();

+ 29 - 0
routes/api/user/register.tsx

@@ -0,0 +1,29 @@
+import { Handlers } from "$fresh/server.ts";
+import {
+  makeErrorResponse,
+  makeSuccessResponse,
+  getCryptoString,
+} from "utils/server.ts";
+import { find, insert } from "utils/db.ts";
+
+export const handler: Handlers = {
+  async POST(req: Request) {
+    const reqJson = await req.json();
+    if (reqJson.email && reqJson.password) {
+      const user = find("User", {
+        email: reqJson.email,
+      });
+      if (user.length === 0) {
+        const newUser = insert("User", {
+          name: reqJson.email.split("@")[0],
+          email: reqJson.email,
+          password: await getCryptoString(reqJson.password, "MD5"),
+        });
+        if (newUser.length > 0) {
+          return makeSuccessResponse(true);
+        }
+      }
+    }
+    return makeErrorResponse();
+  },
+};

+ 62 - 8
routes/index.tsx

@@ -1,13 +1,67 @@
 /** @jsx h */
-import { h } from "preact";
-import TopBar from "../islands/TopBar.tsx";
-import Editor from "../islands/Editor.tsx";
+/** @jsxFrag Fragment */
+import { Fragment, h } from "preact";
+import { Head } from "$fresh/runtime.ts";
+import { Handlers, PageProps } from "$fresh/server.ts";
+import { checkToken } from "utils/server.ts";
+import { find } from "utils/db.ts";
+import HomeBar from "../islands/HomeBar.tsx";
+import PostList from "../islands/PostList.tsx";
 
-export default function Home() {
+interface HomeProps {
+  name: string;
+  list: { id: number; title: string; content: string; shared: boolean }[];
+}
+
+export const handler: Handlers<HomeProps> = {
+  GET(req, ctx) {
+    const tokenUserId = checkToken(req);
+    if (tokenUserId) {
+      const user = find(
+        "User",
+        {
+          id: tokenUserId,
+        },
+        ["name"]
+      );
+      if (user.length > 0) {
+        const posts = find("Post", { user_id: tokenUserId }, [
+          "id",
+          "title",
+          "content",
+          "shared",
+        ]);
+        return ctx.render({
+          name: user[0][0] as string,
+          list: posts.map((post) => ({
+            id: post[0] as number,
+            title: post[1] as string,
+            content: post[2] as string,
+            shared: post[3] as boolean,
+          })),
+        });
+      }
+    }
+    // Redirect to login page if not valid
+    const headers = new Headers();
+    headers.set("location", "/login");
+    return new Response(null, {
+      status: 303, // See Other
+      headers,
+    });
+  },
+};
+
+export default function Home(props: PageProps<HomeProps>) {
   return (
-    <div className="pd-page">
-      <TopBar allowMode="both" />
-      <Editor id="id" allowMode="both" />
-    </div>
+    <>
+      <Head>
+        <title>Home</title>
+      </Head>
+      <div className="pd-page">
+        <HomeBar name={props.data.name} />
+        <PostList posts={props.data.list} />
+      </div>
+    </>
   );
 }

+ 12 - 5
routes/login.tsx

@@ -1,12 +1,19 @@
 /** @jsx h */
-import { h } from "preact";
+/** @jsxFrag Fragment */
+import { Fragment, h } from "preact";
+import { Head } from "$fresh/runtime.ts";
 import LoginFrame from "../islands/LoginFrame.tsx";
 
 export default function Login() {
   return (
-    <div className="pd-page pd-page-centered">
-      <h2>Sign in to Postdown</h2>
-      <LoginFrame />
-    </div>
+    <>
+      <Head>
+        <title>Login</title>
+      </Head>
+      <div className="pd-page pd-page-centered">
+        <h2>Sign in to Postdown</h2>
+        <LoginFrame mode="login" />
+      </div>
+    </>
   );
 }

+ 67 - 0
routes/post/[id].tsx

@@ -0,0 +1,67 @@
+/** @jsx h */
+/** @jsxFrag Fragment */
+import { Fragment, h } from "preact";
+import { Head } from "$fresh/runtime.ts";
+import { Handlers, PageProps } from "$fresh/server.ts";
+import { checkToken } from "utils/server.ts";
+import { find } from "utils/db.ts";
+import TopBar from "../../islands/TopBar.tsx";
+import Editor, { EditorMode } from "../../islands/Editor.tsx";
+
+interface PostProps {
+  id: number;
+  title: string;
+  content: string;
+  isLogined: boolean;
+  allowMode: EditorMode;
+}
+
+export const handler: Handlers<PostProps> = {
+  GET(req, ctx) {
+    const tokenUserId = checkToken(req);
+    const postId = Number(ctx.params.id);
+    const post = find(
+      "Post",
+      tokenUserId
+        ? {
+            id: postId,
+            user_id: tokenUserId,
+          }
+        : { id: postId, shared: true },
+      ["title", "content"]
+    );
+    if (post.length > 0) {
+      return ctx.render({
+        id: postId,
+        isLogined: Boolean(tokenUserId),
+        allowMode: tokenUserId ? EditorMode.Both : EditorMode.Read,
+        title: (post[0][0] as string) || "Untitled",
+        content: post[0][1] as string,
+      });
+    }
+    // Redirect to 404 page if not found
+    return ctx.renderNotFound();
+  },
+};
+
+export default function Post(props: PageProps) {
+  return (
+    <>
+      <Head>
+        <title>{props.data.title}</title>
+      </Head>
+      <div className="pd-page">
+        <TopBar
+          allowMode={props.data.allowMode}
+          isLogined={props.data.isLogined}
+        />
+        <Editor
+          id={props.data.id}
+          title={props.data.title}
+          content={props.data.content}
+          allowMode={props.data.allowMode}
+        />
+      </div>
+    </>
+  );
+}

+ 19 - 0
routes/register.tsx

@@ -0,0 +1,19 @@
+/** @jsx h */
+/** @jsxFrag Fragment */
+import { Fragment, h } from "preact";
+import { Head } from "$fresh/runtime.ts";
+import LoginFrame from "../islands/LoginFrame.tsx";
+
+export default function Register() {
+  return (
+    <>
+      <Head>
+        <title>Register</title>
+      </Head>
+      <div className="pd-page pd-page-centered">
+        <h2>Register to Postdown</h2>
+        <LoginFrame mode="register" />
+      </div>
+    </>
+  );
+}

+ 54 - 0
static/global.css

@@ -5,6 +5,7 @@
 
 .pd-page {
   width: 100vw;
+  min-width: 375px;
   height: 100vh;
   padding: 0.75rem;
   display: flex;
@@ -288,3 +289,56 @@ button {
   display: none;
 }
 /* Editor styles end */
+
+/* HomeBar styles start */
+.pd-home-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.pd-home-bar button {
+  width: unset;
+}
+
+.pd-home-bar .pd-home-user-info button {
+  margin-left: 8px;
+}
+/* HomeBar styles end */
+
+/* PostList styles start */
+.pd-post-list {
+  width: 100%;
+  display: grid;
+  row-gap: 16px;
+  column-gap: 16px;
+  grid-template-columns: auto auto auto auto;
+  margin-top: 16px;
+  padding-bottom: 16px;
+  overflow: auto;
+}
+
+.pd-post-list .pd-post {
+  width: 100%;
+  min-width: 180px;
+  border: 1px solid #dee2e6;
+  border-radius: 0.375rem;
+  box-sizing: border-box;
+  padding: 1rem;
+  display: flex;
+  font-size: 16px;
+  flex-direction: column;
+}
+
+.pd-post-list .pd-post:last-child {
+  margin-right: 0;
+}
+
+.pd-post-list .pd-post span {
+  margin-bottom: 8px;
+}
+
+.pd-post-list .pd-post .pd-post-title {
+  font-weight: 500;
+}
+/* PostList styles end */

+ 106 - 11
utils/db.ts

@@ -9,36 +9,131 @@ function prepareDB(tableName: string) {
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           name VARCHAR(128),
           email VARCHAR(128),
-          password TEXT
+          password TEXT,
+          created DATETIME DEFAULT CURRENT_TIMESTAMP,
+          updated DATETIME,
+          UNIQUE(name)
         )
       `);
       break;
     case "Token":
       db.execute(`
-        CREATE TABLE IF NOT EXISTS session (
+        CREATE TABLE IF NOT EXISTS token (
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           user_id INTEGER,
           token VARCHAR(128),
+          created DATETIME DEFAULT CURRENT_TIMESTAMP,
+          updated DATETIME
         )
       `);
       break;
+    case "Post":
+      db.execute(`
+        CREATE TABLE IF NOT EXISTS post (
+          id INTEGER PRIMARY KEY AUTOINCREMENT,
+          user_id INTEGER,
+          title VARCHAR(256),
+          content TEXT,
+          shared BOOLEAN,
+          created DATETIME DEFAULT CURRENT_TIMESTAMP,
+          updated DATETIME
+        )
+      `);
   }
   return db;
 }
 
 export function find(
   tableName: string,
-  queryObject: { [key: string]: string | number }
+  queryObject: { [key: string]: string | number | boolean },
+  targetKeys: string[] = [],
+  limit?: number
+) {
+  const db = prepareDB(tableName);
+  const findQuery = db.prepareQuery(
+    `SELECT ${
+      targetKeys.length > 0 ? targetKeys.join(", ") : "*"
+    } FROM ${tableName.toLowerCase()} WHERE ${Object.keys(queryObject)
+      .map((queryKey) => `${queryKey} = :${queryKey}`)
+      .join(" AND ")} ORDER BY updated DESC ${limit ? ` LIMIT ${limit}` : ""}`
+  );
+  try {
+    return findQuery.all(queryObject);
+  } catch (e) {
+    console.error("Find error:", e);
+    return [];
+  } finally {
+    findQuery.finalize();
+    db.close();
+  }
+}
+
+export function insert(
+  tableName: string,
+  userInsertObject: { [key: string]: string | number | boolean }
+) {
+  const db = prepareDB(tableName);
+  const insertObject = { ...userInsertObject, updated: new Date() };
+  const insertQuery = db.prepareQuery(
+    `INSERT INTO ${tableName.toLowerCase()} (${Object.keys(insertObject).join(
+      ", "
+    )}) VALUES (${Object.keys(insertObject)
+      .map((key) => `:${key}`)
+      .join(", ")})`
+  );
+  try {
+    insertQuery.all(insertObject);
+    return find(tableName, userInsertObject, ["id"], 1);
+  } catch (e) {
+    console.error("Insert error:", e);
+    return [];
+  } finally {
+    insertQuery.finalize();
+    db.close();
+  }
+}
+
+export function update(
+  tableName: string,
+  id: number,
+  userUpdateObject: { [key: string]: string | number | boolean }
 ) {
   const db = prepareDB(tableName);
-  const queryConditions = Object.keys(queryObject).map((queryKey) =>
-    typeof queryObject[queryKey] === "number"
-      ? `${queryKey}=${queryObject[queryKey]}`
-      : `${queryKey}="${queryObject[queryKey]}"`
+  const updateObject = { ...userUpdateObject, updated: new Date() };
+  const updateQuery = db.prepareQuery(
+    `UPDATE ${tableName.toLowerCase()} SET ${Object.keys(updateObject)
+      .map((updateKey) => `${updateKey} = :${updateKey}`)
+      .join(", ")} WHERE id=${id}`
   );
-  return db.query(
-    `SELECT * FROM ${tableName.toLowerCase()} WHERE ${queryConditions.join(
-      " AND "
-    )}`
+  try {
+    updateQuery.all(updateObject);
+    return find(tableName, userUpdateObject, ["id"], 1);
+  } catch (e) {
+    console.error("Insert error:", e);
+    return [];
+  } finally {
+    updateQuery.finalize();
+    db.close();
+  }
+}
+
+export function del(
+  tableName: string,
+  queryObject: { [key: string]: string | number | boolean }
+) {
+  const db = prepareDB(tableName);
+  const deleteQuery = db.prepareQuery(
+    `DELETE FROM ${tableName.toLowerCase()} WHERE ${Object.keys(queryObject)
+      .map((queryKey) => `${queryKey} = :${queryKey}`)
+      .join(" AND ")}`
   );
+  try {
+    return deleteQuery.all(queryObject);
+  } catch (e) {
+    console.error("Insert error:", e);
+    return [];
+  } finally {
+    deleteQuery.finalize();
+    db.close();
+  }
 }

+ 29 - 5
utils/server.ts

@@ -1,17 +1,36 @@
+import { crypto, toHashString, DigestAlgorithm } from "$crypto/mod.ts";
 import { setCookie, getCookies, deleteCookie } from "$http/cookie.ts";
+import { find } from "utils/db.ts";
+
+export async function getCryptoString(rawString: string, cryptoMethod: string) {
+  const buffer = await crypto.subtle.digest(
+    cryptoMethod as DigestAlgorithm,
+    new TextEncoder().encode(rawString)
+  );
+  return toHashString(buffer);
+}
 
 export function checkToken(req: Request) {
   const cookies = getCookies(req.headers);
-  if (cookies && cookies["pd-user-token"]) {
-    return true;
+  if (cookies) {
+    const token = find(
+      "Token",
+      {
+        token: cookies["pd-user-token"] || "",
+      },
+      ["user_id"]
+    );
+    if (token.length > 0) {
+      return token[0][0] as string;
+    }
   }
   return false;
 }
 
-export function setToken(res: Response) {
+export function setToken(res: Response, token: string) {
   setCookie(res.headers, {
     name: "pd-user-token",
-    value: "testTEST123!@#",
+    value: token,
     path: "/",
   });
 }
@@ -21,7 +40,12 @@ export function clearToken(res: Response) {
 }
 
 export function makeSuccessResponse(
-  data: Record<string, unknown> | string | number | boolean
+  data:
+    | Record<string, unknown>
+    | Record<string, unknown>[]
+    | string
+    | number
+    | boolean
 ) {
   return new Response(
     JSON.stringify({