feat: database context
This commit is contained in:
@ -1,14 +1,32 @@
|
|||||||
import { useEffect, useState, type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { getDeviceId } from "./util/deviceId";
|
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";
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <IndexPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
element: <LoginPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/register",
|
||||||
|
element: <RegisterPage />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
const [deviceId, setDeviceId] = useState("");
|
return (
|
||||||
|
<DbProvider>
|
||||||
useEffect(() => {
|
<RouterProvider router={router} />
|
||||||
getDeviceId().then((id) => setDeviceId(id));
|
</DbProvider>
|
||||||
}, []);
|
);
|
||||||
|
|
||||||
return <div>{deviceId}</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
13
web/src/context/db/db.ts
Normal file
13
web/src/context/db/db.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
interface DbContextValues {
|
||||||
|
db: IDBDatabase | null;
|
||||||
|
setDb: (db: IDBDatabase) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DbContext = createContext<DbContextValues>({
|
||||||
|
db: null,
|
||||||
|
setDb: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useDbContext = () => useContext(DbContext);
|
23
web/src/context/db/provider.tsx
Normal file
23
web/src/context/db/provider.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useCallback, useState, type FC, type ReactNode } from "react";
|
||||||
|
import { DbContext } from "./db";
|
||||||
|
|
||||||
|
interface IDBProvider {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DbProvider: FC<IDBProvider> = ({ children }) => {
|
||||||
|
const [db, _setDb] = useState<IDBDatabase | null>(null);
|
||||||
|
|
||||||
|
const setDb = useCallback((db: IDBDatabase) => _setDb(db), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DbContext.Provider
|
||||||
|
value={{
|
||||||
|
db,
|
||||||
|
setDb,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DbContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,26 +1,8 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import LoginPage from "./pages/Login";
|
|
||||||
import RegisterPage from "./pages/Register";
|
|
||||||
|
|
||||||
const root = document.getElementById("root")!;
|
const root = document.getElementById("root")!;
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
createRoot(root).render(<App />);
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <App />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/login",
|
|
||||||
element: <LoginPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/register",
|
|
||||||
element: <RegisterPage />,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
createRoot(root).render(<RouterProvider router={router} />);
|
|
||||||
|
30
web/src/pages/Index/index.tsx
Normal file
30
web/src/pages/Index/index.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { deriveDeviceKey, getDeviceId } from "@/util/deviceId";
|
||||||
|
import { useEffect, useState, type FC } from "react";
|
||||||
|
|
||||||
|
const IndexPage: FC = () => {
|
||||||
|
const [deviceId, setDeviceId] = useState("");
|
||||||
|
const [deviceKey, setDeviceKey] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDeviceId().then((id) => {
|
||||||
|
setDeviceId(id);
|
||||||
|
deriveDeviceKey(id).then((key) => {
|
||||||
|
crypto.subtle.exportKey("raw", key).then((key) => {
|
||||||
|
const enc = new TextDecoder();
|
||||||
|
setDeviceKey(enc.decode(key));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Id:</p>
|
||||||
|
<p>{deviceId}</p>
|
||||||
|
<p>Key:</p>
|
||||||
|
<p>{deviceKey}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndexPage;
|
30
web/src/util/account.ts
Normal file
30
web/src/util/account.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { deriveDeviceKey, getDeviceId } from "./deviceId";
|
||||||
|
import { encryptToken } from "./token";
|
||||||
|
|
||||||
|
const storeTokensForAccount = async (
|
||||||
|
accountId: string,
|
||||||
|
accessToken: string,
|
||||||
|
refreshToken: string
|
||||||
|
) => {
|
||||||
|
const deviceKeyId = await getDeviceId();
|
||||||
|
const key = await deriveDeviceKey(deviceKeyId);
|
||||||
|
|
||||||
|
const access = await encryptToken(accessToken, key);
|
||||||
|
const refresh = await encryptToken(refreshToken, key);
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
accountId,
|
||||||
|
label: `Account for ${accountId}`,
|
||||||
|
access: {
|
||||||
|
data: Array.from(new Uint8Array(access.cipherText)),
|
||||||
|
iv: Array.from(access.iv),
|
||||||
|
},
|
||||||
|
refresh: {
|
||||||
|
data: Array.from(new Uint8Array(refresh.cipherText)),
|
||||||
|
iv: Array.from(refresh.iv),
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save this `entry` in IndexedDB (or use a localforage wrapper)
|
||||||
|
};
|
@ -29,3 +29,26 @@ export const getDeviceId = async () => {
|
|||||||
|
|
||||||
return deviceId; // A 64-character hex string
|
return deviceId; // A 64-character hex string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deriveDeviceKey = async (deviceKeyId: string) => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const baseKey = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
encoder.encode(deviceKeyId),
|
||||||
|
{ name: "PBKDF2" },
|
||||||
|
false,
|
||||||
|
["deriveKey"]
|
||||||
|
);
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: "PBKDF2",
|
||||||
|
salt: encoder.encode("guard_salt"),
|
||||||
|
iterations: 100000,
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
baseKey,
|
||||||
|
{ name: "AES-GCM", length: 256 },
|
||||||
|
false,
|
||||||
|
["encrypt", "decrypt"]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
18
web/src/util/token.ts
Normal file
18
web/src/util/token.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export type EncryptedToken = {
|
||||||
|
cipherText: ArrayBuffer;
|
||||||
|
iv: Uint8Array<ArrayBuffer>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptToken = async (
|
||||||
|
token: string,
|
||||||
|
key: CryptoKey
|
||||||
|
): Promise<EncryptedToken> => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const cipherText = await crypto.subtle.encrypt(
|
||||||
|
{ name: "AES-GCM", iv },
|
||||||
|
key,
|
||||||
|
encoder.encode(token)
|
||||||
|
);
|
||||||
|
return { cipherText, iv };
|
||||||
|
};
|
Reference in New Issue
Block a user