Compare commits
2 Commits
eaf3596580
...
eb42b61b2c
Author | SHA1 | Date | |
---|---|---|---|
eb42b61b2c | |||
06e0e90677 |
@ -126,6 +126,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
// fields required for UI in account selector, e.g. email, full name and avatar
|
// fields required for UI in account selector, e.g. email, full name and avatar
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
Id string `json:"id"`
|
||||||
// Avatar
|
// Avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +135,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
FullName: user.FullName,
|
FullName: user.FullName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
|
Id: user.ID.String(),
|
||||||
// Avatar
|
// Avatar
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { type FC } from "react";
|
import { useEffect, type FC } from "react";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import { DbProvider } from "./context/db/provider";
|
|
||||||
|
|
||||||
import IndexPage from "./pages/Index";
|
import IndexPage from "./pages/Index";
|
||||||
import LoginPage from "./pages/Login";
|
import LoginPage from "./pages/Login";
|
||||||
import RegisterPage from "./pages/Register";
|
import RegisterPage from "./pages/Register";
|
||||||
|
import { useDbContext } from "./context/db/db";
|
||||||
|
import { openDB } from "idb";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -22,11 +23,23 @@ const router = createBrowserRouter([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
return (
|
const { db, setDb } = useDbContext();
|
||||||
<DbProvider>
|
|
||||||
<RouterProvider router={router} />
|
useEffect(() => {
|
||||||
</DbProvider>
|
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;
|
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";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
interface DbContextValues {
|
interface DbContextValues {
|
||||||
db: IDBDatabase | null;
|
db: IDBPDatabase | null;
|
||||||
setDb: (db: IDBDatabase) => void;
|
setDb: (db: IDBPDatabase) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DbContext = createContext<DbContextValues>({
|
export const DbContext = createContext<DbContextValues>({
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { useCallback, useState, type FC, type ReactNode } from "react";
|
import { useCallback, useState, type FC, type ReactNode } from "react";
|
||||||
import { DbContext } from "./db";
|
import { DbContext } from "./db";
|
||||||
|
import type { IDBPDatabase } from "idb";
|
||||||
|
|
||||||
interface IDBProvider {
|
interface IDBProvider {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DbProvider: FC<IDBProvider> = ({ children }) => {
|
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 (
|
return (
|
||||||
<DbContext.Provider
|
<DbContext.Provider
|
||||||
|
@ -2,7 +2,12 @@ import { createRoot } from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { DbProvider } from "./context/db/provider";
|
||||||
|
|
||||||
const root = document.getElementById("root")!;
|
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 { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Mail, Lock } from "lucide-react";
|
import { Mail, Lock } from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@ -7,6 +8,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
import overlay from "@/assets/overlay.jpg";
|
import overlay from "@/assets/overlay.jpg";
|
||||||
import { useForm, type SubmitHandler } from "react-hook-form";
|
import { useForm, type SubmitHandler } from "react-hook-form";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import { loginApi } from "@/api/login";
|
||||||
|
import { useAccountRepo } from "@/repository/account";
|
||||||
|
|
||||||
interface LoginForm {
|
interface LoginForm {
|
||||||
email: string;
|
email: string;
|
||||||
@ -25,6 +28,8 @@ export default function LoginPage() {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
|
|
||||||
|
const repo = useAccountRepo();
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LoginForm> = useCallback(
|
const onSubmit: SubmitHandler<LoginForm> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
console.log({ data });
|
console.log({ data });
|
||||||
@ -34,36 +39,34 @@ export default function LoginPage() {
|
|||||||
setSuccess("");
|
setSuccess("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/v1/login", {
|
const response = await loginApi({
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (response.status != 200) {
|
|
||||||
const json = await response.json();
|
console.log(response);
|
||||||
const text = json.error || "Unexpected error happened";
|
|
||||||
setError(
|
await repo.save({
|
||||||
`Failed to create an account. ${
|
accountId: response.id,
|
||||||
text[0].toUpperCase() + text.slice(1)
|
label: response.full_name,
|
||||||
}`
|
email: response.email,
|
||||||
);
|
access: response.access,
|
||||||
} else {
|
refresh: response.refresh,
|
||||||
|
});
|
||||||
|
|
||||||
setSuccess("You have successfully logged in");
|
setSuccess("You have successfully logged in");
|
||||||
reset();
|
reset();
|
||||||
}
|
} catch (err: any) {
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
setError("Failed to create account. Unexpected error happened");
|
setError(
|
||||||
|
"Failed to create account. " +
|
||||||
|
(err.message ?? "Unexpected error happened")
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[reset]
|
[repo, reset]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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