feat: store logged account
This commit is contained in:
@ -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 (
|
||||
<DbProvider>
|
||||
<RouterProvider router={router} />
|
||||
</DbProvider>
|
||||
);
|
||||
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 <RouterProvider router={router} />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
20
web/src/api/index.ts
Normal file
20
web/src/api/index.ts
Normal file
@ -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");
|
||||
};
|
34
web/src/api/login.ts
Normal file
34
web/src/api/login.ts
Normal file
@ -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;
|
||||
};
|
@ -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<DbContextValues>({
|
||||
|
@ -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<IDBProvider> = ({ children }) => {
|
||||
const [db, _setDb] = useState<IDBDatabase | null>(null);
|
||||
const [db, _setDb] = useState<IDBPDatabase | null>(null);
|
||||
|
||||
const setDb = useCallback((db: IDBDatabase) => _setDb(db), []);
|
||||
const setDb = useCallback((db: IDBPDatabase) => _setDb(db), []);
|
||||
|
||||
return (
|
||||
<DbContext.Provider
|
||||
|
@ -2,7 +2,12 @@ import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
import "./index.css";
|
||||
import { DbProvider } from "./context/db/provider";
|
||||
|
||||
const root = document.getElementById("root")!;
|
||||
|
||||
createRoot(root).render(<App />);
|
||||
createRoot(root).render(
|
||||
<DbProvider>
|
||||
<App />
|
||||
</DbProvider>
|
||||
);
|
||||
|
@ -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<LoginForm> = 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 (
|
||||
|
64
web/src/repository/account.ts
Normal file
64
web/src/repository/account.ts
Normal file
@ -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 };
|
||||
};
|
Reference in New Issue
Block a user