diff --git a/web/src/App.tsx b/web/src/App.tsx
index 6c8f751..e95ac6a 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,10 +1,11 @@
-import { type FC } from "react";
+import { useEffect, type FC } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
-import { DbProvider } from "./context/db/provider";
import IndexPage from "./pages/Index";
import LoginPage from "./pages/Login";
import RegisterPage from "./pages/Register";
+import { useDbContext } from "./context/db/db";
+import { openDB } from "idb";
const router = createBrowserRouter([
{
@@ -22,11 +23,23 @@ const router = createBrowserRouter([
]);
const App: FC = () => {
- return (
-
-
-
- );
+ const { db, setDb } = useDbContext();
+
+ useEffect(() => {
+ const openConnection = async () => {
+ const conn = await openDB("guard-local", 3);
+
+ if (!conn.objectStoreNames.contains("accounts")) {
+ conn.createObjectStore("accounts", { keyPath: "accountId" });
+ }
+
+ setDb(conn);
+ };
+
+ openConnection();
+ }, [db, setDb]);
+
+ return ;
};
export default App;
diff --git a/web/src/api/index.ts b/web/src/api/index.ts
new file mode 100644
index 0000000..e8c9123
--- /dev/null
+++ b/web/src/api/index.ts
@@ -0,0 +1,20 @@
+export const handleApiError = async (response: Response) => {
+ try {
+ const json = await response.json();
+ console.log({ json });
+ const text = json.error ?? "unexpected error happpened";
+ return new Error(text[0].toUpperCase() + text.slice(1));
+ } catch (err) {
+ try {
+ console.log(err);
+ const text = await response.text();
+ if (text.length > 0) {
+ return new Error(text[0].toUpperCase() + text.slice(1));
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ return new Error("Unexpected error happened");
+};
diff --git a/web/src/api/login.ts b/web/src/api/login.ts
new file mode 100644
index 0000000..7539e91
--- /dev/null
+++ b/web/src/api/login.ts
@@ -0,0 +1,34 @@
+import { handleApiError } from ".";
+
+export interface LoginRequest {
+ email: string;
+ password: string;
+}
+
+export interface LoginResponse {
+ id: string;
+ email: string;
+ full_name: string;
+ access: string;
+ refresh: string;
+}
+
+export const loginApi = async (req: LoginRequest) => {
+ const response = await fetch("/api/v1/login", {
+ method: "POST",
+ body: JSON.stringify({
+ email: req.email,
+ password: req.password,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (response.status !== 200 && response.status !== 201)
+ throw await handleApiError(response);
+
+ const data: LoginResponse = await response.json();
+
+ return data;
+};
diff --git a/web/src/context/db/db.ts b/web/src/context/db/db.ts
index ae9dc95..89957d8 100644
--- a/web/src/context/db/db.ts
+++ b/web/src/context/db/db.ts
@@ -1,8 +1,9 @@
+import type { IDBPDatabase } from "idb";
import { createContext, useContext } from "react";
interface DbContextValues {
- db: IDBDatabase | null;
- setDb: (db: IDBDatabase) => void;
+ db: IDBPDatabase | null;
+ setDb: (db: IDBPDatabase) => void;
}
export const DbContext = createContext({
diff --git a/web/src/context/db/provider.tsx b/web/src/context/db/provider.tsx
index f25f499..f6d41bc 100644
--- a/web/src/context/db/provider.tsx
+++ b/web/src/context/db/provider.tsx
@@ -1,14 +1,15 @@
import { useCallback, useState, type FC, type ReactNode } from "react";
import { DbContext } from "./db";
+import type { IDBPDatabase } from "idb";
interface IDBProvider {
children: ReactNode;
}
export const DbProvider: FC = ({ children }) => {
- const [db, _setDb] = useState(null);
+ const [db, _setDb] = useState(null);
- const setDb = useCallback((db: IDBDatabase) => _setDb(db), []);
+ const setDb = useCallback((db: IDBPDatabase) => _setDb(db), []);
return (
);
+createRoot(root).render(
+
+
+
+);
diff --git a/web/src/pages/Login/index.tsx b/web/src/pages/Login/index.tsx
index 56d94c0..316d9e3 100644
--- a/web/src/pages/Login/index.tsx
+++ b/web/src/pages/Login/index.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
import { Card, CardContent } from "@/components/ui/card";
import { Mail, Lock } from "lucide-react";
import { Link } from "react-router-dom";
@@ -7,6 +8,8 @@ import { Button } from "@/components/ui/button";
import overlay from "@/assets/overlay.jpg";
import { useForm, type SubmitHandler } from "react-hook-form";
import { useCallback, useState } from "react";
+import { loginApi } from "@/api/login";
+import { useAccountRepo } from "@/repository/account";
interface LoginForm {
email: string;
@@ -25,6 +28,8 @@ export default function LoginPage() {
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
+ const repo = useAccountRepo();
+
const onSubmit: SubmitHandler = useCallback(
async (data) => {
console.log({ data });
@@ -34,36 +39,34 @@ export default function LoginPage() {
setSuccess("");
try {
- const response = await fetch("/api/v1/login", {
- method: "POST",
- body: JSON.stringify({
- email: data.email,
- password: data.password,
- }),
- headers: {
- "Content-Type": "application/json",
- },
+ const response = await loginApi({
+ email: data.email,
+ password: data.password,
});
- if (response.status != 200) {
- const json = await response.json();
- const text = json.error || "Unexpected error happened";
- setError(
- `Failed to create an account. ${
- text[0].toUpperCase() + text.slice(1)
- }`
- );
- } else {
- setSuccess("You have successfully logged in");
- reset();
- }
- } catch (err) {
+
+ console.log(response);
+
+ await repo.save({
+ accountId: response.id,
+ label: response.full_name,
+ email: response.email,
+ access: response.access,
+ refresh: response.refresh,
+ });
+
+ setSuccess("You have successfully logged in");
+ reset();
+ } catch (err: any) {
console.log(err);
- setError("Failed to create account. Unexpected error happened");
+ setError(
+ "Failed to create account. " +
+ (err.message ?? "Unexpected error happened")
+ );
} finally {
setLoading(false);
}
},
- [reset]
+ [repo, reset]
);
return (
diff --git a/web/src/repository/account.ts b/web/src/repository/account.ts
new file mode 100644
index 0000000..1226213
--- /dev/null
+++ b/web/src/repository/account.ts
@@ -0,0 +1,64 @@
+import { useDbContext } from "@/context/db/db";
+import { deriveDeviceKey, getDeviceId } from "@/util/deviceId";
+import { useCallback } from "react";
+
+export interface LocalAccount {
+ accountId: string;
+ label: string;
+ email: string;
+ access: { data: number[]; iv: number[] };
+ refresh: { data: number[]; iv: number[] };
+ updatedAt: string;
+}
+
+export interface CreateAccountRequest {
+ accountId: string;
+ label: string;
+ email: string;
+ access: string;
+ refresh: string;
+}
+
+export const useAccountRepo = () => {
+ const { db } = useDbContext();
+
+ const encryptToken = useCallback(async (token: string) => {
+ const encoder = new TextEncoder();
+
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+
+ const deviceId = await getDeviceId();
+ const deviceKey = await deriveDeviceKey(deviceId);
+
+ const cipherText = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv },
+ deviceKey,
+ encoder.encode(token)
+ );
+
+ return {
+ data: Array.from(new Uint8Array(cipherText)),
+ iv: Array.from(iv),
+ };
+ }, []);
+
+ const save = useCallback(
+ async (req: CreateAccountRequest) => {
+ console.log({ db });
+ const access = await encryptToken(req.access);
+ const refresh = await encryptToken(req.refresh);
+
+ await db?.put?.("accounts", {
+ accountId: req.accountId,
+ label: req.label,
+ email: req.email,
+ access,
+ refresh,
+ updatedAt: new Date().toISOString(),
+ });
+ },
+ [db, encryptToken]
+ );
+
+ return { save };
+};