Compare commits
7 Commits
83c26bb94a
...
78e84567c7
Author | SHA1 | Date | |
---|---|---|---|
78e84567c7 | |||
0423b3803f | |||
60e317b9e4 | |||
aa18b9f3e2 | |||
d9ca1ce2b4 | |||
41c3dfdfe4 | |||
725cc74102 |
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
5
web/.prettierignore
Normal file
@ -0,0 +1,5 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
node_modules
|
||||
public
|
1
web/.prettierrc
Normal file
1
web/.prettierrc
Normal file
@ -0,0 +1 @@
|
||||
{}
|
@ -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,
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
17
web/package-lock.json
generated
17
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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) => {
|
||||
|
@ -15,7 +15,7 @@ export const refreshTokenApi = async (refreshToken: string) => {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${refreshToken}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 200 && response.status !== 201)
|
||||
|
@ -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) => (
|
||||
|
104
web/src/components/Home/Tabs/ApiServices.tsx
Normal file
104
web/src/components/Home/Tabs/ApiServices.tsx
Normal 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;
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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) => (
|
||||
|
@ -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",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
@ -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 (
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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") ?? "");
|
||||
|
@ -9,5 +9,5 @@ const root = document.getElementById("root")!;
|
||||
createRoot(root).render(
|
||||
<OAuthProvider>
|
||||
<App />
|
||||
</OAuthProvider>
|
||||
</OAuthProvider>,
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 };
|
||||
|
@ -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
|
||||
|
@ -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"],
|
||||
);
|
||||
};
|
||||
|
@ -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 };
|
||||
};
|
||||
|
Reference in New Issue
Block a user