Compare commits
6 Commits
ef05d66787
...
c7e88606e3
Author | SHA1 | Date | |
---|---|---|---|
c7e88606e3 | |||
a0d506fb76 | |||
0ec7743fca | |||
a8a0fa55b7 | |||
7321448ce7 | |||
6d5e0fc9a9 |
@ -27,11 +27,13 @@ const processRefreshQueue = async (token: string | null) => {
|
|||||||
|
|
||||||
const logout = async (accountId: string) => {
|
const logout = async (accountId: string) => {
|
||||||
const db = useDbStore.getState().db;
|
const db = useDbStore.getState().db;
|
||||||
const requireSignIn = useAuth.getState().requireSignIn;
|
const { requireSignIn, loadAccounts } = useAuth.getState();
|
||||||
|
|
||||||
if (db) {
|
if (db) {
|
||||||
await deleteAccount(db, accountId);
|
await deleteAccount(db, accountId);
|
||||||
}
|
}
|
||||||
|
await loadAccounts();
|
||||||
|
|
||||||
requireSignIn?.();
|
requireSignIn?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
21
web/src/api/signout.ts
Normal file
21
web/src/api/signout.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { handleApiError } from ".";
|
||||||
|
|
||||||
|
export const signoutApi = async (token: string) => {
|
||||||
|
const response = await axios.post(
|
||||||
|
"/api/v1/auth/signout",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200 && response.status !== 201)
|
||||||
|
throw await handleApiError(response);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
@ -7,17 +7,32 @@ const Sidebar: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden sm:flex flex-col gap-2 items-stretch border-r border-gray-300 dark:border-gray-700 min-w-80 w-80 p-5 pt-18 min-h-screen select-none">
|
<div className="hidden sm:flex flex-col gap-2 items-stretch border-r border-gray-300 dark:border-gray-700 min-w-80 w-80 p-5 pt-18 min-h-screen select-none">
|
||||||
{barItems.map((item) => (
|
{barItems.map((item, index) =>
|
||||||
<Link to={item.pathname} key={item.tab}>
|
item.type !== "delimiter" ? (
|
||||||
|
<Link to={item.pathname} key={item.tab}>
|
||||||
|
<div
|
||||||
|
className={`dark:text-gray-200 transition-colors text-sm cursor-pointer p-4 rounded-lg flex flex-row items-center gap-3${
|
||||||
|
isActive(item) ? " bg-gray-200 dark:bg-gray-900" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.icon} {item.title}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`dark:text-gray-200 transition-colors text-sm cursor-pointer p-4 rounded-lg flex flex-row items-center gap-3${
|
key={item.key}
|
||||||
isActive(item) ? " bg-gray-200 dark:bg-gray-900" : ""
|
className={`flex flex-row items-center gap-4 my-2 ${index === 0 ? "mt-0" : "mt-4"}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{item.icon} {item.title}
|
<div className="w-full h-[2px] rounded-lg bg-gray-800"></div>
|
||||||
|
{typeof item.title === "string" && (
|
||||||
|
<p className="text-gray-800 dark:text-gray-400 text-sm">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="w-full h-[2px] rounded-lg bg-gray-800"></div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
),
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,19 +7,21 @@ const TopBar: FC = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
{barItems.map((item) => (
|
{barItems
|
||||||
<Link to={item.pathname} key={item.tab}>
|
.filter((item) => item.type !== "delimiter")
|
||||||
<div
|
.map((item) => (
|
||||||
className={`flex-shrink-0 transition-all border-b-4 px-4 py-2 min-w-[120px] sm:min-w-0 sm:flex-1 flex items-center justify-center cursor-pointer select-none whitespace-nowrap text-sm font-medium ${
|
<Link to={item.pathname} key={item.tab}>
|
||||||
isActive(item)
|
<div
|
||||||
? " border-b-4 border-b-blue-500 text-blue-500"
|
className={`flex-shrink-0 transition-all border-b-4 px-4 py-2 min-w-[120px] sm:min-w-0 sm:flex-1 flex items-center justify-center cursor-pointer select-none whitespace-nowrap text-sm font-medium ${
|
||||||
: " border-b-transparent text-gray-500"
|
isActive(item)
|
||||||
}`}
|
? " border-b-4 border-b-blue-500 text-blue-500"
|
||||||
>
|
: " border-b-transparent text-gray-500"
|
||||||
{item.title}
|
}`}
|
||||||
</div>
|
>
|
||||||
</Link>
|
{item.title}
|
||||||
))}
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
import { useAuth } from "@/store/auth";
|
import { useAuth } from "@/store/auth";
|
||||||
import { Blocks, Home, Settings2, User, Users } from "lucide-react";
|
import { Blocks, Home, User, Users } from "lucide-react";
|
||||||
import { useCallback, type ReactNode } from "react";
|
import { useCallback, type ReactNode } from "react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
export interface BarDelimiter {
|
||||||
|
type: "delimiter";
|
||||||
|
key: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BarItem {
|
export interface BarItem {
|
||||||
|
type?: "nav";
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
tab: string;
|
tab: string;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBarItems = (): [BarItem[], (item: BarItem) => boolean] => {
|
export type Item = BarItem | BarDelimiter;
|
||||||
|
|
||||||
|
export const useBarItems = (): [Item[], (item: Item) => boolean] => {
|
||||||
const profile = useAuth((state) => state.profile);
|
const profile = useAuth((state) => state.profile);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isActive = useCallback(
|
const isActive = useCallback(
|
||||||
(item: BarItem) => {
|
(item: Item) => {
|
||||||
|
if (item.type === "delimiter") return false;
|
||||||
if (item.pathname === "/") return location.pathname === item.pathname;
|
if (item.pathname === "/") return location.pathname === item.pathname;
|
||||||
return location.pathname.startsWith(item.pathname);
|
return location.pathname.startsWith(item.pathname);
|
||||||
},
|
},
|
||||||
@ -28,6 +38,11 @@ export const useBarItems = (): [BarItem[], (item: BarItem) => boolean] => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
type: "delimiter" as const,
|
||||||
|
title: "Basic",
|
||||||
|
key: "basic-del",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: <Home />,
|
icon: <Home />,
|
||||||
title: "Home",
|
title: "Home",
|
||||||
@ -40,14 +55,20 @@ export const useBarItems = (): [BarItem[], (item: BarItem) => boolean] => {
|
|||||||
tab: "personal-info",
|
tab: "personal-info",
|
||||||
pathname: "/personal-info",
|
pathname: "/personal-info",
|
||||||
},
|
},
|
||||||
{
|
// TODO:
|
||||||
icon: <Settings2 />,
|
// {
|
||||||
title: "Data & Personalization",
|
// icon: <Settings2 />,
|
||||||
tab: "data-personalization",
|
// title: "Data & Personalization",
|
||||||
pathname: "/data-personalize",
|
// tab: "data-personalization",
|
||||||
},
|
// pathname: "/data-personalize",
|
||||||
|
// },
|
||||||
...(profile.is_admin
|
...(profile.is_admin
|
||||||
? [
|
? [
|
||||||
|
{
|
||||||
|
type: "delimiter" as const,
|
||||||
|
title: "Admin",
|
||||||
|
key: "admin-del",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: <Blocks />,
|
icon: <Blocks />,
|
||||||
title: "API Services",
|
title: "API Services",
|
||||||
|
@ -5,7 +5,7 @@ import ApiServiceCredentialsModal from "@/feature/ApiServiceCredentialsModal";
|
|||||||
import { useApiServices } from "@/store/admin/apiServices";
|
import { useApiServices } from "@/store/admin/apiServices";
|
||||||
import { useCallback, type FC } from "react";
|
import { useCallback, type FC } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Link } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
name: string;
|
name: string;
|
||||||
@ -32,10 +32,12 @@ const ApiServiceCreatePage: FC = () => {
|
|||||||
|
|
||||||
const credentials = useApiServices((state) => state.createdCredentials);
|
const credentials = useApiServices((state) => state.createdCredentials);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(data: FormData) => {
|
async (data: FormData) => {
|
||||||
console.log("Form submitted:", data);
|
console.log("Form submitted:", data);
|
||||||
createApiService({
|
const success = await createApiService({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description ?? "",
|
description: data.description ?? "",
|
||||||
redirect_uris: data.redirectUris.trim().split("\n"),
|
redirect_uris: data.redirectUris.trim().split("\n"),
|
||||||
@ -45,8 +47,11 @@ const ApiServiceCreatePage: FC = () => {
|
|||||||
: ["authorization_code"],
|
: ["authorization_code"],
|
||||||
is_active: data.enabled,
|
is_active: data.enabled,
|
||||||
});
|
});
|
||||||
|
if (success) {
|
||||||
|
navigate("/admin/api-services");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[createApiService],
|
[createApiService, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { useUsers } from "@/store/admin/users";
|
import { useUsers } from "@/store/admin/users";
|
||||||
import { useCallback, type FC } from "react";
|
import { useCallback, type FC } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Link } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
fullName: string;
|
fullName: string;
|
||||||
@ -24,17 +24,22 @@ const AdminCreateUserPage: FC = () => {
|
|||||||
|
|
||||||
const createUser = useUsers((state) => state.createUser);
|
const createUser = useUsers((state) => state.createUser);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(data: FormData) => {
|
async (data: FormData) => {
|
||||||
console.log("Form submitted:", data);
|
console.log("Form submitted:", data);
|
||||||
createUser({
|
const success = await createUser({
|
||||||
email: data.email,
|
email: data.email,
|
||||||
full_name: data.fullName,
|
full_name: data.fullName,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
is_admin: data.isAdmin,
|
is_admin: data.isAdmin,
|
||||||
});
|
});
|
||||||
|
if (success) {
|
||||||
|
navigate("/admin/users");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[createUser],
|
[createUser, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -22,7 +22,7 @@ interface IApiServicesState {
|
|||||||
|
|
||||||
fetch: () => Promise<void>;
|
fetch: () => Promise<void>;
|
||||||
fetchSingle: (id: string) => Promise<void>;
|
fetchSingle: (id: string) => Promise<void>;
|
||||||
create: (req: CreateApiServiceRequest) => Promise<void>;
|
create: (req: CreateApiServiceRequest) => Promise<bool>;
|
||||||
resetCredentials: () => void;
|
resetCredentials: () => void;
|
||||||
|
|
||||||
toggling: boolean;
|
toggling: boolean;
|
||||||
@ -117,11 +117,12 @@ export const useApiServices = create<IApiServicesState>((set, get) => ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await postApiService(req);
|
const response = await postApiService(req);
|
||||||
set({ createdCredentials: response.credentials });
|
set({ createdCredentials: response.credentials, creating: false });
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("ERR: Failed to fetch services:", err);
|
console.log("ERR: Failed to fetch services:", err);
|
||||||
} finally {
|
|
||||||
set({ creating: false });
|
set({ creating: false });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -15,7 +15,7 @@ export interface IUsersState {
|
|||||||
fetchingCurrent: boolean;
|
fetchingCurrent: boolean;
|
||||||
|
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
createUser: (req: CreateUserRequest) => Promise<void>;
|
createUser: (req: CreateUserRequest) => Promise<boolean>;
|
||||||
|
|
||||||
fetchUsers: () => Promise<void>;
|
fetchUsers: () => Promise<void>;
|
||||||
fetchUser: (id: string) => Promise<void>;
|
fetchUser: (id: string) => Promise<void>;
|
||||||
@ -36,10 +36,12 @@ export const useUsers = create<IUsersState>((set) => ({
|
|||||||
try {
|
try {
|
||||||
const response = await postUser(req);
|
const response = await postUser(req);
|
||||||
console.log("INFO: User has been created:", response);
|
console.log("INFO: User has been created:", response);
|
||||||
|
set({ creating: false });
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("ERR: Failed to create user:", err);
|
console.log("ERR: Failed to create user:", err);
|
||||||
} finally {
|
|
||||||
set({ creating: false });
|
set({ creating: false });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user