feat: database context

This commit is contained in:
2025-05-24 11:19:16 +02:00
parent b8f3fa0a32
commit eaf3596580
8 changed files with 165 additions and 28 deletions

View File

@ -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
View 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);

View 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>
);
};

View File

@ -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 />);

View 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
View 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)
};

View File

@ -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
View 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 };
};