Compare commits

...

7 Commits

31 changed files with 282 additions and 121 deletions

View File

@ -46,7 +46,14 @@ func (s *APIServer) Run() error {
router.Route("/api/v1", func(r chi.Router) {
am := imiddleware.New(s.cfg)
r.Use(imiddleware.WithSkipper(am.Runner, "/api/v1/auth/login", "/api/v1/register", "/api/v1/auth/refresh", "/api/v1/oauth/token"))
r.Use(imiddleware.WithSkipper(
am.Runner,
"/api/v1/auth/login",
"/api/v1/register",
"/api/v1/auth/refresh",
"/api/v1/oauth/token",
"/api/v1/avatar",
))
userHandler := user.NewUserHandler(s.repo, s.storage)
userHandler.RegisterRoutes(r)

View File

@ -34,6 +34,10 @@ func (fs *FileStorage) PutObject(ctx context.Context, bucketName string, objectN
return fs.client.PutObject(ctx, bucketName, objectName, reader, size, opts)
}
func (fs *FileStorage) GetObject(ctx context.Context, bucketName string, objectName string, opts minio.GetObjectOptions) (*minio.Object, error) {
return fs.client.GetObject(ctx, bucketName, objectName, opts)
}
func (fs *FileStorage) EndpointURL() *url.URL {
return fs.client.EndpointURL()
}

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"path/filepath"
"strings"
@ -34,6 +36,7 @@ func NewUserHandler(repo *repository.Queries, minio *storage.FileStorage) *UserH
func (h *UserHandler) RegisterRoutes(api chi.Router) {
api.Post("/register", h.register)
api.Put("/avatar", h.uploadAvatar)
api.Get("/avatar/{avatar}", h.getAvatar)
}
type RegisterParams struct {
@ -93,6 +96,28 @@ func (h *UserHandler) register(w http.ResponseWriter, r *http.Request) {
}
}
func (h *UserHandler) getAvatar(w http.ResponseWriter, r *http.Request) {
avatarObject := chi.URLParam(r, "avatar")
object, err := h.minio.GetObject(r.Context(), "guard-storage", avatarObject, minio.GetObjectOptions{})
if err != nil {
web.Error(w, "avatar not found", http.StatusNotFound)
return
}
defer object.Close()
stat, err := object.Stat()
if err != nil {
log.Printf("ERR: failed to get object stats: %v\n", err)
web.Error(w, "failed to get avatar", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", stat.ContentType)
w.WriteHeader(http.StatusOK)
io.Copy(w, object)
}
func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
userId, ok := util.GetRequestUserId(r.Context())
if !ok {
@ -134,11 +159,9 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
return
}
imageURL := fmt.Sprintf("http://%s/%s/%s", h.minio.EndpointURL().Host, "guard-storage", uploadInfo.Key)
if err := h.repo.UpdateProfilePicture(r.Context(), repository.UpdateProfilePictureParams{
ProfilePicture: pgtype.Text{
String: imageURL,
String: uploadInfo.Key,
Valid: true,
},
ID: user.ID,
@ -148,14 +171,14 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
}
type Response struct {
URL string `json:"url"`
AvatarID string `json:"url"`
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(Response{URL: imageURL}); err != nil {
if err := encoder.Encode(Response{AvatarID: uploadInfo.Key}); err != nil {
web.Error(w, "failed to write response", http.StatusInternalServerError)
}
}

5
web/.prettierignore Normal file
View File

@ -0,0 +1,5 @@
# Ignore artifacts:
build
coverage
node_modules
public

1
web/.prettierrc Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -24,31 +24,31 @@ export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
},
})
});
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
"react-x": reactX,
"react-dom": reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactX.configs["recommended-typescript"].rules,
...reactDom.configs.recommended.rules,
},
})
});
```

View File

@ -1,28 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ['dist'] },
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
)
);

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

17
web/package-lock.json generated
View File

@ -34,6 +34,7 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"path": "^0.12.7",
"prettier": "3.5.3",
"sass": "^1.89.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
@ -4262,6 +4263,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",

View File

@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build:watch": "tsc -b && vite build --watch",
"lint": "eslint .",
"preview": "vite preview"
},
@ -36,6 +37,7 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"path": "^0.12.7",
"prettier": "3.5.3",
"sass": "^1.89.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",

View File

@ -13,7 +13,7 @@ export const codeApi = async (accessToken: string, nonce: string) => {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
}
},
);
if (response.status !== 200 && response.status !== 201)

View File

@ -28,7 +28,7 @@ const processRefreshQueue = async (token: string | null) => {
const refreshToken = async (
accountId: string,
refreshToken: string
refreshToken: string,
): Promise<{ access: string; refresh: string }> => {
const db = useDbStore.getState().db;
const loadAccounts = useAuth.getState().loadAccounts;
@ -79,7 +79,7 @@ axios.interceptors.request.use(
try {
const { access } = await refreshToken(
account!.accountId,
account!.refresh
account!.refresh,
);
token = access;
} catch (err) {
@ -97,7 +97,7 @@ axios.interceptors.request.use(
request.headers["Authorization"] = `Bearer ${token}`;
return request;
},
(error) => Promise.reject(error)
(error) => Promise.reject(error),
);
export const handleApiError = async (response: AxiosResponse) => {

View File

@ -15,7 +15,7 @@ export const refreshTokenApi = async (refreshToken: string) => {
"Content-Type": "application/json",
Authorization: `Bearer ${refreshToken}`,
},
}
},
);
if (response.status !== 200 && response.status !== 201)

View File

@ -1,5 +1,5 @@
import type { FC } from "react";
import { barItems } from "../tabs";
import { useBarItems } from "../tabs";
export interface ISidebarProps {
activeTab: string;
@ -7,6 +7,8 @@ export interface ISidebarProps {
}
const Sidebar: FC<ISidebarProps> = ({ activeTab, onChangeTab }) => {
const barItems = useBarItems();
return (
<div className="hidden sm:flex flex-col gap-2 items-stretch min-w-80 w-80 p-5 pt-18 min-h-screen select-none bg-white/65 dark:bg-black/65 shadow-lg shadow-gray-300 dark:shadow-gray-700">
{barItems.map((item) => (

View File

@ -0,0 +1,104 @@
import type { FC } from "react";
import { Link } from "react-router";
const services = [
{
id: "1",
name: "User Service",
clientId: "user-svc-001",
isActive: true,
createdAt: "2024-09-15T10:20:30Z",
updatedAt: "2025-01-10T12:00:00Z",
},
{
id: "2",
name: "Billing Service",
clientId: "billing-svc-009",
isActive: false,
createdAt: "2024-10-01T08:45:10Z",
updatedAt: "2025-03-22T14:30:00Z",
},
{
id: "3",
name: "Analytics Service",
clientId: "analytics-svc-777",
isActive: true,
createdAt: "2024-11-25T16:00:00Z",
updatedAt: "2025-02-05T10:15:45Z",
},
{
id: "4",
name: "Email Service",
clientId: "email-svc-333",
isActive: false,
createdAt: "2023-07-10T13:00:00Z",
updatedAt: "2024-12-31T09:25:00Z",
},
];
const ApiServices: FC = () => {
return (
<div className="overflow-x-auto rounded shadow-md dark:shadow-gray-800">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
Name
</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
Client ID
</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
Is Active
</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
Created At
</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
Updated At
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{services.map((service) => (
<tr
key={service.id}
className="hover:bg-gray-50 dark:hover:bg-gray-800"
>
<td className="px-6 py-4 text-sm font-medium text-blue-600">
<Link
to={`/services/${service.id}`}
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
{service.name}
</Link>
</td>
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{service.clientId}
</td>
<td className="px-6 py-4 text-sm">
<span
className={`inline-block px-2 py-1 text-xs rounded-full font-semibold ${
service.isActive
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
}`}
>
{service.isActive ? "Yes" : "No"}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{new Date(service.createdAt).toLocaleString()}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{new Date(service.updatedAt).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default ApiServices;

View File

@ -1,25 +1,16 @@
import { Button } from "@/components/ui/button";
import Avatar from "@/feature/Avatar";
import { useAuth } from "@/store/auth";
import { User } from "lucide-react";
import { type FC } from "react";
const Home: FC = () => {
const profile = useAuth((state) => state.profile);
console.log({ profile });
const signOut = useAuth((state) => state.signOut);
return (
<div className="flex flex-col items-center gap-2 p-7">
<div className="w-24 h-24 sm:w-36 sm:h-36 overflow-hidden rounded-full flex items-center justify-center bg-gray-300">
{profile?.profile_picture ? (
<img
className="w-full h-full object-cover"
src={profile.profile_picture}
alt="profile pic"
/>
) : (
<User size={64} />
)}
<Avatar iconSize={64} />
</div>
<h1 className="dark:text-gray-200 text-gray-800 text-2xl select-none">
Welcome, {profile?.full_name}

View File

@ -1,7 +1,11 @@
import Avatar from "@/feature/Avatar";
import { useAuth } from "@/store/auth";
import { ChevronRight } from "lucide-react";
import { type FC } from "react";
const PersonalInfo: FC = () => {
const profile = useAuth((state) => state.profile);
return (
<>
<h1 className="dark:text-gray-200 text-gray-800 text-2xl">
@ -35,11 +39,7 @@ const PersonalInfo: FC = () => {
</div>
<div>
<div className="w-16 h-16 overflow-hidden rounded-full dark:bg-gray-400 bg-gray-700">
<img
className="w-full h-full"
src="http://192.168.178.69:9000/guard-storage/profile_eff00028-2d9e-458d-8944-677855edc147_1748099702417601900.jpg"
alt="profile pic"
/>
<Avatar iconSize={12} />
</div>
</div>
</div>
@ -50,7 +50,9 @@ const PersonalInfo: FC = () => {
<p className="text-sm dark:text-gray-400 font-medium text-gray-600">
Name
</p>
<p className="text dark:text-gray-200 text-gray-800">Amir Adal</p>
<p className="text dark:text-gray-200 text-gray-800">
{profile?.full_name}
</p>
</div>
<div>
<div className="text-gray-500">

View File

@ -1,5 +1,5 @@
import { type FC } from "react";
import { barItems } from "../tabs";
import { useBarItems } from "../tabs";
export interface ITopBarProps {
activeTab: string;
@ -7,6 +7,8 @@ export interface ITopBarProps {
}
const TopBar: FC<ITopBarProps> = ({ activeTab, onChangeTab }) => {
const barItems = useBarItems();
return (
<div className="sm:hidden flex w-full overflow-x-auto sm:overflow-x-visible max-w-full min-w-full sm:justify-center sm:space-x-4 no-scrollbar shadow-md shadow-gray-300 dark:shadow-gray-700 dark:bg-black/70 bg-white/70 pt-14">
{barItems.map((item) => (

View File

@ -1,19 +1,37 @@
import { Home, Settings2, User } from "lucide-react";
import { useAuth } from "@/store/auth";
import { Blocks, Home, Settings2, User } from "lucide-react";
export const barItems = [
{
icon: <Home />,
title: "Home",
tab: "home",
},
{
icon: <User />,
title: "Personal Info",
tab: "personal-info",
},
{
icon: <Settings2 />,
title: "Data & Personalization",
tab: "data-personalization",
},
];
export const useBarItems = () => {
const profile = useAuth((state) => state.profile);
if (!profile) {
return [];
}
return [
{
icon: <Home />,
title: "Home",
tab: "home",
},
{
icon: <User />,
title: "Personal Info",
tab: "personal-info",
},
{
icon: <Settings2 />,
title: "Data & Personalization",
tab: "data-personalization",
},
...(profile.isAdmin
? [
{
icon: <Blocks />,
title: "API Services",
tab: "api-services",
},
]
: []),
];
};

View File

@ -19,8 +19,6 @@ export const OAuthProvider: FC<IOAuthProvider> = ({ children }) => {
if (active && redirectURI) {
const codeResponse = await codeApi(token, nonce);
console.log("gen code:", { codeResponse });
const params = new URLSearchParams({
code: codeResponse.code,
state,
@ -29,7 +27,7 @@ export const OAuthProvider: FC<IOAuthProvider> = ({ children }) => {
window.location.replace(`${redirectURI}?${params.toString()}`);
}
},
[active, nonce, redirectURI, state]
[active, nonce, redirectURI, state],
);
return (

View File

@ -1,8 +1,9 @@
import { type LocalAccount } from "@/repository/account";
import { useAuth } from "@/store/auth";
import { CirclePlus, User } from "lucide-react";
import { CirclePlus } from "lucide-react";
import { useCallback, type FC } from "react";
import { Link, useLocation } from "react-router";
import Avatar from "../Avatar";
const AccountList: FC = () => {
const accounts = useAuth((state) => state.accounts);
@ -14,7 +15,7 @@ const AccountList: FC = () => {
(account: LocalAccount) => {
updateActiveAccount(account);
},
[updateActiveAccount]
[updateActiveAccount],
);
return (
@ -27,17 +28,7 @@ const AccountList: FC = () => {
>
<div>
<div className="rounded-full w-10 h-10 overflow-hidden bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-200 mr-3 ring ring-gray-400 dark:ring dark:ring-gray-500">
{account.profilePicture ? (
<img
src={account.profilePicture}
className="w-full h-full flex-1 object-cover"
alt="profile"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<User />
</div>
)}
<Avatar iconSize={8} avatarId={account.profilePicture ?? null} />
</div>
</div>
<div className="flex flex-col">

View File

@ -1,22 +1,28 @@
import { useAuth } from "@/store/auth";
import { User } from "lucide-react";
import type { FC } from "react";
import { useMemo, type FC } from "react";
export interface AvatarProps {
iconSize?: number;
className?: string;
avatarId?: string;
}
const Avatar: FC<AvatarProps> = ({ iconSize = 32, className }) => {
const Avatar: FC<AvatarProps> = ({ iconSize = 32, className, avatarId }) => {
const profile = useAuth((state) => state.profile);
const avatar = useMemo(
() => (avatarId !== undefined ? avatarId : profile?.profile_picture),
[avatarId, profile?.profile_picture],
);
return (
<div
className={`overflow-hidden bg-gray-100 rounded-full ring ring-gray-400 dark:ring dark:ring-gray-500 flex items-center justify-center ${className}`}
>
{profile?.profile_picture ? (
{avatar ? (
<img
src={profile?.profile_picture?.toString()}
src={`/api/v1/avatar/${avatar?.toString()}`}
className="w-full h-full flex-1 object-cover"
alt="profile"
/>

View File

@ -71,10 +71,6 @@ const AuthLayout = () => {
useEffect(() => {
if (!active) {
console.log(
"setting search params:",
Object.fromEntries(searchParams.entries())
);
setActive(true);
setClientID(searchParams.get("client_id") ?? "");
setRedirectURI(searchParams.get("redirect_uri") ?? "");

View File

@ -9,5 +9,5 @@ const root = document.getElementById("root")!;
createRoot(root).render(
<OAuthProvider>
<App />
</OAuthProvider>
</OAuthProvider>,
);

View File

@ -8,6 +8,7 @@ import Sidebar from "@/components/Home/Sidebar";
import TopBar from "@/components/Home/TopBar";
import Home from "@/components/Home/Tabs/Home";
import PersonalInfo from "@/components/Home/Tabs/PersonalInfo";
import ApiServices from "@/components/Home/Tabs/ApiServices";
const IndexPage: FC = () => {
const [tab, setTab] = useState<string>("home");
@ -38,6 +39,7 @@ const IndexPage: FC = () => {
</div>
<div className="p-4">
{tab === "personal-info" && <PersonalInfo />}
{tab === "api-services" && <ApiServices />}
</div>
</div>
</div>

View File

@ -36,8 +36,6 @@ export default function LoginPage() {
const onSubmit: SubmitHandler<LoginForm> = useCallback(
async (data) => {
console.log({ data });
setLoading(true);
setError("");
setSuccess("");
@ -48,8 +46,6 @@ export default function LoginPage() {
password: data.password,
});
console.log(response);
const account = await repo.save({
accountId: response.id,
label: response.full_name,
@ -67,13 +63,13 @@ export default function LoginPage() {
console.log(err);
setError(
"Failed to create account. " +
(err.message ?? "Unexpected error happened")
(err.message ?? "Unexpected error happened"),
);
} finally {
setLoading(false);
}
},
[repo, reset, updateActiveAccount]
[repo, reset, updateActiveAccount],
);
return (

View File

@ -31,8 +31,6 @@ export default function RegisterPage() {
const onSubmit: SubmitHandler<RegisterForm> = useCallback(
async (data) => {
console.log({ data });
setLoading(true);
setError("");
setSuccess("");
@ -56,11 +54,11 @@ export default function RegisterPage() {
setError(
`Failed to create an account. ${
text[0].toUpperCase() + text.slice(1)
}`
}`,
);
} else {
setSuccess(
"Account has been created. You can now log into your new account"
"Account has been created. You can now log into your new account",
);
reset();
}
@ -71,7 +69,7 @@ export default function RegisterPage() {
setLoading(false);
}
},
[reset]
[reset],
);
return (

View File

@ -61,7 +61,7 @@ export const encryptToken = async (token: string) => {
const cipherText = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
deviceKey,
encoder.encode(token)
encoder.encode(token),
);
return {
@ -84,7 +84,7 @@ export const decryptToken = async (encrypted: {
iv: new Uint8Array(encrypted.iv),
},
deviceKey,
new Uint8Array(encrypted.data)
new Uint8Array(encrypted.data),
);
return decoder.decode(decrypted);
@ -92,7 +92,7 @@ export const decryptToken = async (encrypted: {
export const saveAccount = async (
db: IDBPDatabase,
req: CreateAccountRequest
req: CreateAccountRequest,
): Promise<LocalAccount> => {
const access = await encryptToken(req.access);
const refresh = await encryptToken(req.refresh);
@ -141,7 +141,7 @@ export const getAllAccounts = async (db: IDBPDatabase) => {
} catch (err) {
console.warn(`Failed to decrypt account ${account.label}:`, err);
}
})
}),
)
).filter((acc) => acc !== undefined);
@ -186,7 +186,7 @@ export const getAccountRaw = async (db: IDBPDatabase, accountId: string) => {
export const updateAccountTokens = async (
db: IDBPDatabase,
req: UpdateAccountTokensRequest
req: UpdateAccountTokensRequest,
) => {
const account = await getAccountRaw(db, req.accountId);
@ -204,7 +204,7 @@ export const updateAccountTokens = async (
export const updateAccountInfo = async (
db: IDBPDatabase,
req: UpdateAccountInfoRequest
req: UpdateAccountInfoRequest,
) => {
const account = await getAccountRaw(db, req.accountId);
await db?.put?.("accounts", {
@ -229,7 +229,7 @@ export const useAccountRepo = () => {
return saveAccount(db, req);
},
[db]
[db],
);
const loadAll = useCallback(async () => {
@ -244,7 +244,7 @@ export const useAccountRepo = () => {
return getAccount(db, accountId);
},
[db]
[db],
);
return { save, loadAll, getOne };

View File

@ -72,8 +72,6 @@ export const useAuth = create<IAuthState>((set, get) => ({
const accounts = await getAllAccounts(db);
console.log("loaded accounts:", accounts);
if (!accounts || accounts.length === 0) {
set({ signInRequired: true });
}
@ -114,7 +112,6 @@ export const useAuth = create<IAuthState>((set, get) => ({
try {
const response = await fetchProfileApi();
console.log("authenticate response:", response);
try {
// update local account information

View File

@ -12,7 +12,6 @@ export const getDeviceId = async () => {
navigator.language, // Primary language
navigator.maxTouchPoints, // Touch capability
];
console.log(fingerprintParts);
const rawFingerprint = fingerprintParts.join("|");
const encoder = new TextEncoder();
@ -37,7 +36,7 @@ export const deriveDeviceKey = async (deviceKeyId: string) => {
encoder.encode(deviceKeyId),
{ name: "PBKDF2" },
false,
["deriveKey"]
["deriveKey"],
);
return crypto.subtle.deriveKey(
{
@ -49,6 +48,6 @@ export const deriveDeviceKey = async (deviceKeyId: string) => {
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
["encrypt", "decrypt"],
);
};

View File

@ -5,14 +5,14 @@ export type EncryptedToken = {
export const encryptToken = async (
token: string,
key: CryptoKey
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)
encoder.encode(token),
);
return { cipherText, iv };
};