feat: database context
This commit is contained in:
@ -1,14 +1,32 @@
|
||||
import { useEffect, useState, type FC } from "react";
|
||||
import { getDeviceId } from "./util/deviceId";
|
||||
import { 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";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <IndexPage />,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
element: <RegisterPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
const App: FC = () => {
|
||||
const [deviceId, setDeviceId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
getDeviceId().then((id) => setDeviceId(id));
|
||||
}, []);
|
||||
|
||||
return <div>{deviceId}</div>;
|
||||
return (
|
||||
<DbProvider>
|
||||
<RouterProvider router={router} />
|
||||
</DbProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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 { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import App from "./App";
|
||||
|
||||
import "./index.css";
|
||||
import LoginPage from "./pages/Login";
|
||||
import RegisterPage from "./pages/Register";
|
||||
|
||||
const root = document.getElementById("root")!;
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
element: <RegisterPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
createRoot(root).render(<RouterProvider router={router} />);
|
||||
createRoot(root).render(<App />);
|
||||
|
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
|
||||
};
|
||||
|
||||
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