Compare commits

...

6 Commits

Author SHA1 Message Date
c7e88606e3 feat: return status of request 2025-06-13 21:46:06 +02:00
a0d506fb76 feat: navigate to list page after successful create 2025-06-13 21:45:53 +02:00
0ec7743fca feat: delimiter handling/support 2025-06-13 21:45:39 +02:00
a8a0fa55b7 feat: delimiter def 2025-06-13 21:45:32 +02:00
7321448ce7 fix: reload accounts 2025-06-11 21:06:36 +02:00
6d5e0fc9a9 feat: signout API 2025-06-11 21:00:22 +02:00
9 changed files with 118 additions and 44 deletions

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -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 (

View File

@ -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 (

View File

@ -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;
} }
}, },
})); }));

View File

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