- |
- {templateData[EmailTemplateInputDataFields.EVENT_NAME]}
- |
- {templateData[EmailTemplateInputDataFields.SUBJECT]} |
-
+
+ {loading ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : emailTemplatesData.length ? (
+ <>
+
+
+
+ Event Name
+ Subject
+ Created At
+ Actions
+
+
+
+ {emailTemplatesData.map((templateData) => (
+
+
+ {templateData.event_name}
+
+
+ {templateData.subject}
+
+
{dayjs(templateData.created_at * 1000).format(
'MMM DD, YYYY',
)}
-
- |
-
- |
-
+
+
+
+
))}
-
- {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
-
-
-
-
-
- paginationHandler({
- page: 1,
- })
- }
- isDisabled={paginationProps.page <= 1}
- mr={4}
- icon={}
- />
-
-
-
- paginationHandler({
- page: paginationProps.page - 1,
- })
- }
- isDisabled={paginationProps.page <= 1}
- icon={}
- />
-
-
-
-
- Page{' '}
-
- {paginationProps.page}
- {' '}
- of{' '}
-
- {paginationProps.maxPages}
-
-
-
- Go to page:{' '}
-
- paginationHandler({
- page: parseInt(value),
- })
- }
- value={paginationProps.page}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- paginationHandler({
- page: paginationProps.page + 1,
- })
- }
- isDisabled={
- paginationProps.page >= paginationProps.maxPages
- }
- icon={}
- />
-
-
-
- paginationHandler({
- page: paginationProps.maxPages,
- })
- }
- isDisabled={
- paginationProps.page >= paginationProps.maxPages
- }
- ml={4}
- icon={}
- />
-
-
-
-
- )}
+
- ) : (
-
-
-
-
-
- No Data
-
-
- )
+
+ {/* Pagination */}
+ {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
+
+
+
+
+
+
+
+
+ Page {paginationProps.page} of{' '}
+ {paginationProps.maxPages}
+
+
+ Go to:
+
+ paginationHandler({
+ page: parseInt(e.target.value) || 1,
+ })
+ }
+ className="h-8 w-16"
+ />
+
+
+
+
+
+
+
+
+
+ )}
+ >
) : (
-
-
-
+
)}
-
+
);
};
diff --git a/web/dashboard/src/pages/Home.tsx b/web/dashboard/src/pages/Home.tsx
deleted file mode 100644
index b3243a864..000000000
--- a/web/dashboard/src/pages/Home.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Text } from '@chakra-ui/react';
-import React from 'react';
-
-export default function Home() {
- return (
- <>
-
- Hi there 👋
-
-
-
- Welcome to Authorizer Administrative Dashboard!
- Manage your users, webhooks, and email templates.
-
- >
- );
-}
diff --git a/web/dashboard/src/pages/Overview.tsx b/web/dashboard/src/pages/Overview.tsx
new file mode 100644
index 000000000..5132df93c
--- /dev/null
+++ b/web/dashboard/src/pages/Overview.tsx
@@ -0,0 +1,232 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useClient } from 'urql';
+import { Users, Activity, Copy, Check, ArrowRight } from 'lucide-react';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from '../components/ui/card';
+import { Button } from '../components/ui/button';
+import { Skeleton } from '../components/ui/skeleton';
+import {
+ UserDetailsQuery,
+ MetaQuery,
+ AuditLogsQuery,
+} from '../graphql/queries';
+import type {
+ UsersResponse,
+ MetaResponse,
+ AuditLogsResponse,
+ AuditLog,
+} from '../types';
+import { copyTextToClipboard } from '../utils';
+import { toast } from 'sonner';
+
+dayjs.extend(relativeTime);
+
+const Overview = () => {
+ const client = useClient();
+ const navigate = useNavigate();
+
+ const [loading, setLoading] = useState(true);
+ const [totalUsers, setTotalUsers] = useState(0);
+ const [meta, setMeta] = useState(null);
+ const [recentActivity, setRecentActivity] = useState([]);
+ const [copiedClientId, setCopiedClientId] = useState(false);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ try {
+ const [usersRes, metaRes, auditRes] = await Promise.all([
+ client
+ .query(UserDetailsQuery, {
+ params: { pagination: { limit: 1, page: 1 } },
+ })
+ .toPromise(),
+ client.query(MetaQuery, {}).toPromise(),
+ client
+ .query(AuditLogsQuery, {
+ params: { pagination: { limit: 5, page: 1 } },
+ })
+ .toPromise(),
+ ]);
+
+ if (usersRes.data?._users) {
+ setTotalUsers(usersRes.data._users.pagination.total);
+ }
+
+ if (metaRes.data?.meta) {
+ setMeta(metaRes.data.meta);
+ }
+
+ if (auditRes.data?._audit_logs) {
+ setRecentActivity(auditRes.data._audit_logs.audit_logs || []);
+ }
+ } catch {
+ toast.error('Failed to load dashboard data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [client]);
+
+ const handleCopyClientId = async () => {
+ if (meta?.client_id) {
+ await copyTextToClipboard(meta.client_id);
+ setCopiedClientId(true);
+ toast.success('Client ID copied');
+ setTimeout(() => setCopiedClientId(false), 2000);
+ }
+ };
+
+ const getActionColor = (action: string): string => {
+ if (action.startsWith('admin.')) return 'bg-purple-100 text-purple-700';
+ if (action.includes('login_success') || action.includes('signup'))
+ return 'bg-green-100 text-green-700';
+ if (action.includes('failed') || action.includes('revoked'))
+ return 'bg-red-100 text-red-700';
+ return 'bg-gray-100 text-gray-700';
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ Overview
+
+ Welcome to your Authorizer dashboard.
+
+
+
+
+ navigate('/users')}
+ >
+
+
+ Total Users
+
+
+
+
+ {totalUsers}
+ View all users →
+
+
+
+
+
+
+ Version
+
+
+
+
+
+ {meta?.version || '—'}
+
+ Authorizer server
+
+
+
+
+
+
+ Client ID
+
+
+
+
+
+ {meta?.client_id || '—'}
+
+ Click icon to copy
+
+
+
+
+
+
+
+ Recent Activity
+
+
+
+
+ {recentActivity.length === 0 ? (
+
+ No recent activity recorded.
+
+ ) : (
+
+ {recentActivity.map((log) => (
+
+
+
+ {log.action}
+
+
+ {log.actor_email || log.actor_id || '—'}
+
+
+
+ {dayjs.unix(log.created_at).fromNow()}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+export default Overview;
diff --git a/web/dashboard/src/pages/Users.tsx b/web/dashboard/src/pages/Users.tsx
index 34b72aa1e..c85bf0a36 100644
--- a/web/dashboard/src/pages/Users.tsx
+++ b/web/dashboard/src/pages/Users.tsx
@@ -1,52 +1,51 @@
import React from 'react';
import { useClient } from 'urql';
import dayjs from 'dayjs';
+import { toast } from 'sonner';
import {
- Box,
- Flex,
- IconButton,
- NumberDecrementStepper,
- NumberIncrementStepper,
- NumberInput,
- NumberInputField,
- NumberInputStepper,
- Select,
- Table,
- Tag,
- Tbody,
- Td,
- Text,
- TableCaption,
- Th,
- Thead,
- Tooltip,
- Tr,
- Button,
- Center,
- Menu,
- MenuButton,
- MenuList,
- MenuItem,
- useToast,
- Spinner,
- TableContainer,
-} from '@chakra-ui/react';
-import {
- FaAngleLeft,
- FaAngleRight,
- FaAngleDoubleLeft,
- FaAngleDoubleRight,
- FaExclamationCircle,
- FaAngleDown,
-} from 'react-icons/fa';
+ ChevronsLeft,
+ ChevronsRight,
+ ChevronLeft,
+ ChevronRight,
+ ChevronDown,
+ AlertCircle,
+ Search,
+} from 'lucide-react';
import { UserDetailsQuery } from '../graphql/queries';
import { EnableAccess, RevokeAccess, UpdateUser } from '../graphql/mutation';
import { getGraphQLErrorMessage } from '../utils';
import EditUserModal from '../components/EditUserModal';
import DeleteUserModal from '../components/DeleteUserModal';
import InviteMembersModal from '../components/InviteMembersModal';
+import ViewUserModal from '../components/ViewUserModal';
+import { Button } from '../components/ui/button';
+import { Badge } from '../components/ui/badge';
+import { Input } from '../components/ui/input';
+import { Select } from '../components/ui/select';
+import { Skeleton } from '../components/ui/skeleton';
+import {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+} from '../components/ui/tooltip';
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+} from '../components/ui/dropdown-menu';
+import {
+ Table,
+ TableHeader,
+ TableBody,
+ TableRow,
+ TableHead,
+ TableCell,
+} from '../components/ui/table';
+import type { User, UsersResponse } from '../types';
-interface paginationPropTypes {
+interface PaginationProps {
limit: number;
page: number;
offset: number;
@@ -54,68 +53,41 @@ interface paginationPropTypes {
maxPages: number;
}
-interface userDataTypes {
- id: string;
- email: string;
- email_verified: boolean;
- given_name: string;
- family_name: string;
- middle_name: string;
- nickname: string;
- gender: string;
- birthdate: string;
- phone_number: string;
- phone_number_verified: boolean;
- picture: string;
- signup_methods: string;
- roles: [string];
- created_at: number;
- revoked_timestamp: number;
- is_multi_factor_auth_enabled?: boolean;
-}
-
-const enum updateAccessActions {
+const enum UpdateAccessActions {
REVOKE = 'REVOKE',
ENABLE = 'ENABLE',
}
-const getMaxPages = (pagination: paginationPropTypes) => {
+const getMaxPages = (pagination: PaginationProps) => {
const { limit, total } = pagination;
if (total > 1) {
- return total % limit === 0
- ? total / limit
- : parseInt(`${total / limit}`) + 1;
- } else return 1;
-};
-
-const getLimits = (pagination: paginationPropTypes) => {
- const { total } = pagination;
- const limits = [5];
- if (total > 10) {
- for (let i = 10; i <= total && limits.length <= 10; i += 5) {
- limits.push(i);
- }
+ return total % limit === 0 ? total / limit : Math.floor(total / limit) + 1;
}
- return limits;
+ return 1;
};
+const PAGE_SIZE_OPTIONS = [10, 25, 50];
+
export default function Users() {
const client = useClient();
- const toast = useToast();
- const [paginationProps, setPaginationProps] =
- React.useState({
- limit: 5,
+ const [paginationProps, setPaginationProps] = React.useState(
+ {
+ limit: 10,
page: 1,
offset: 0,
total: 0,
maxPages: 1,
- });
- const [userList, setUserList] = React.useState([]);
+ },
+ );
+ const [userList, setUserList] = React.useState([]);
const [loading, setLoading] = React.useState(false);
+ const [searchQuery, setSearchQuery] = React.useState('');
+ const [selectedUser, setSelectedUser] = React.useState(null);
+
const updateUserList = async () => {
setLoading(true);
const { data } = await client
- .query(UserDetailsQuery, {
+ .query(UserDetailsQuery, {
params: {
pagination: {
limit: paginationProps.limit,
@@ -126,7 +98,7 @@ export default function Users() {
.toPromise();
if (data?._users) {
const { pagination, users } = data._users;
- const maxPages = getMaxPages(pagination);
+ const maxPages = getMaxPages(pagination as unknown as PaginationProps);
if (users && users.length > 0) {
setPaginationProps({ ...paginationProps, ...pagination, maxPages });
setUserList(users);
@@ -143,9 +115,11 @@ export default function Users() {
}
setLoading(false);
};
+
React.useEffect(() => {
updateUserList();
}, []);
+
React.useEffect(() => {
updateUserList();
}, [paginationProps.page, paginationProps.limit]);
@@ -154,106 +128,71 @@ export default function Users() {
setPaginationProps({ ...paginationProps, ...value });
};
- const userVerificationHandler = async (user: userDataTypes) => {
+ const userVerificationHandler = async (user: User) => {
const { id, email, phone_number } = user;
- let params = {};
+ let params: Record = {};
if (email) {
- params = {
- id,
- email,
- email_verified: true,
- };
+ params = { id, email, email_verified: true };
}
if (phone_number) {
- params = {
- id,
- phone_number,
- phone_number_verified: true,
- };
+ params = { id, phone_number, phone_number_verified: true };
}
- const res = await client
- .mutation(UpdateUser, {
- params,
- })
- .toPromise();
+ const res = await client.mutation(UpdateUser, { params }).toPromise();
if (res.error) {
- toast({
- title: getGraphQLErrorMessage(res.error, 'User verification failed'),
- isClosable: true,
- status: 'error',
- position: 'top-right',
- });
+ toast.error(
+ getGraphQLErrorMessage(res.error, 'User verification failed'),
+ );
} else if (res.data?._update_user?.id) {
- toast({
- title: 'User verification successful',
- isClosable: true,
- status: 'success',
- position: 'top-right',
- });
+ toast.success('User verification successful');
}
updateUserList();
};
const updateAccessHandler = async (
id: string,
- action: updateAccessActions,
+ action: UpdateAccessActions,
) => {
switch (action) {
- case updateAccessActions.ENABLE:
+ case UpdateAccessActions.ENABLE: {
const enableAccessRes = await client
- .mutation(EnableAccess, {
- param: {
- user_id: id,
- },
- })
+ .mutation(EnableAccess, { param: { user_id: id } })
.toPromise();
if (enableAccessRes.error) {
- toast({
- title: getGraphQLErrorMessage(enableAccessRes.error, 'User access enable failed'),
- isClosable: true,
- status: 'error',
- position: 'top-right',
- });
+ toast.error(
+ getGraphQLErrorMessage(
+ enableAccessRes.error,
+ 'User access enable failed',
+ ),
+ );
} else {
- toast({
- title: 'User access enabled successfully',
- isClosable: true,
- status: 'success',
- position: 'top-right',
- });
+ toast.success('User access enabled successfully');
}
updateUserList();
break;
- case updateAccessActions.REVOKE:
+ }
+ case UpdateAccessActions.REVOKE: {
const revokeAccessRes = await client
- .mutation(RevokeAccess, {
- param: {
- user_id: id,
- },
- })
+ .mutation(RevokeAccess, { param: { user_id: id } })
.toPromise();
if (revokeAccessRes.error) {
- toast({
- title: getGraphQLErrorMessage(revokeAccessRes.error, 'User access revoke failed'),
- isClosable: true,
- status: 'error',
- position: 'top-right',
- });
+ toast.error(
+ getGraphQLErrorMessage(
+ revokeAccessRes.error,
+ 'User access revoke failed',
+ ),
+ );
} else {
- toast({
- title: 'User access revoked successfully',
- isClosable: true,
- status: 'success',
- position: 'top-right',
- });
+ toast.success('User access revoked successfully');
}
updateUserList();
break;
+ }
default:
break;
}
};
- const multiFactorAuthUpdateHandler = async (user: userDataTypes) => {
+
+ const multiFactorAuthUpdateHandler = async (user: User) => {
const res = await client
.mutation(UpdateUser, {
params: {
@@ -263,334 +202,326 @@ export default function Users() {
})
.toPromise();
if (res.data?._update_user?.id) {
- toast({
- title: `Multi factor authentication ${
+ toast.success(
+ `Multi factor authentication ${
user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled'
} for user`,
- isClosable: true,
- status: 'success',
- position: 'top-right',
- });
+ );
updateUserList();
return;
}
if (res.error) {
- toast({
- title: getGraphQLErrorMessage(res.error, 'Multi factor authentication update failed for user'),
- isClosable: true,
- status: 'error',
- position: 'top-right',
- });
+ toast.error(
+ getGraphQLErrorMessage(
+ res.error,
+ 'Multi factor authentication update failed for user',
+ ),
+ );
}
};
+ const filteredUsers = userList.filter(
+ (user) =>
+ searchQuery === '' ||
+ (user.email || '').toLowerCase().includes(searchQuery.toLowerCase()),
+ );
+
return (
-
-
-
- Users
-
+
+
+
+ Users
+
+ Manage users, roles, and access.
+
+
-
- {!loading ? (
- userList.length > 0 ? (
-
-
-
-
- | Email / Phone |
- Created At |
- Signup Methods |
- Roles |
- Verified |
- Access |
-
-
- MFA
-
- |
- Actions |
-
-
-
- {userList.map((user: userDataTypes) => {
- const {
- email_verified,
- phone_number_verified,
- created_at,
- ...rest
- }: any = user;
- return (
-
- | {user.email || user.phone_number} |
-
- {dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
- |
- {user.signup_methods} |
- {user.roles.join(', ')} |
-
-
- {(
- user.email_verified || user.phone_number_verified
- ).toString()}
-
- |
-
-
- {user.revoked_timestamp ? 'Revoked' : 'Enabled'}
-
- |
-
-
- {user.is_multi_factor_auth_enabled
- ? 'Enabled'
- : 'Disabled'}
-
- |
-
-
- |
-
- );
- })}
-
- {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
-
-
-
-
-
- paginationHandler({
- page: 1,
- })
- }
- isDisabled={paginationProps.page <= 1}
- mr={4}
- icon={}
- />
-
-
-
- paginationHandler({
- page: paginationProps.page - 1,
- })
- }
- isDisabled={paginationProps.page <= 1}
- icon={}
- />
-
-
-
-
- Page{' '}
-
- {paginationProps.page}
- {' '}
- of{' '}
-
- {paginationProps.maxPages}
-
-
-
- Go to page:{' '}
-
- paginationHandler({
- page: parseInt(value),
- })
- }
- value={paginationProps.page}
- >
-
-
-
-
-
-
-
-
+
+ {/* Pagination */}
+ {(paginationProps.maxPages > 1 || paginationProps.total >= 10) && (
+
+
+
+
+
+
+
+
+ Page {paginationProps.page} of{' '}
+ {paginationProps.maxPages}
+
+
+ Go to:
+
+ paginationHandler({
+ page: parseInt(e.target.value) || 1,
+ })
+ }
+ className="h-8 w-16"
+ />
+
+
+
+
+
+
+
+
+
+ )}
+ >
) : (
-
-
-
+
)}
-
+ setSelectedUser(null)}
+ />
+
);
}
diff --git a/web/dashboard/src/pages/Webhooks.tsx b/web/dashboard/src/pages/Webhooks.tsx
index 9d903788f..cfd9645ce 100644
--- a/web/dashboard/src/pages/Webhooks.tsx
+++ b/web/dashboard/src/pages/Webhooks.tsx
@@ -1,51 +1,48 @@
import React, { useEffect, useState } from 'react';
import { useClient } from 'urql';
import {
- Box,
- Button,
- Center,
- Flex,
- IconButton,
- Menu,
- MenuButton,
- MenuList,
- NumberDecrementStepper,
- NumberIncrementStepper,
- NumberInput,
- NumberInputField,
- NumberInputStepper,
- Select,
- Spinner,
- Table,
- TableCaption,
- Tag,
- Tbody,
- Td,
- Text,
- Th,
- Thead,
- Tooltip,
- Tr,
-} from '@chakra-ui/react';
-import {
- FaAngleDoubleLeft,
- FaAngleDoubleRight,
- FaAngleDown,
- FaAngleLeft,
- FaAngleRight,
- FaExclamationCircle,
-} from 'react-icons/fa';
+ ChevronsLeft,
+ ChevronsRight,
+ ChevronLeft,
+ ChevronRight,
+ ChevronDown,
+ AlertCircle,
+} from 'lucide-react';
import UpdateWebhookModal from '../components/UpdateWebhookModal';
import {
- pageLimits,
+ pageLimitsExtended,
WebhookInputDataFields,
UpdateModalViews,
} from '../constants';
import { WebhooksDataQuery } from '../graphql/queries';
import DeleteWebhookModal from '../components/DeleteWebhookModal';
import ViewWebhookLogsModal from '../components/ViewWebhookLogsModal';
+import { Button } from '../components/ui/button';
+import { Badge } from '../components/ui/badge';
+import { Input } from '../components/ui/input';
+import { Select } from '../components/ui/select';
+import { Skeleton } from '../components/ui/skeleton';
+import {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+} from '../components/ui/tooltip';
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+} from '../components/ui/dropdown-menu';
+import {
+ Table,
+ TableHeader,
+ TableBody,
+ TableRow,
+ TableHead,
+ TableCell,
+} from '../components/ui/table';
+import type { Webhook, WebhooksResponse } from '../types';
-interface paginationPropTypes {
+interface PaginationProps {
limit: number;
page: number;
offset: number;
@@ -53,38 +50,32 @@ interface paginationPropTypes {
maxPages: number;
}
-interface webhookDataTypes {
- [WebhookInputDataFields.ID]: string;
- [WebhookInputDataFields.EVENT_NAME]: string;
- [WebhookInputDataFields.EVENT_DESCRIPTION]?: string;
- [WebhookInputDataFields.ENDPOINT]: string;
- [WebhookInputDataFields.ENABLED]: boolean;
- [WebhookInputDataFields.HEADERS]?: Record ;
-}
-
const Webhooks = () => {
const client = useClient();
const [loading, setLoading] = useState(false);
- const [webhookData, setWebhookData] = useState([]);
- const [paginationProps, setPaginationProps] = useState({
- limit: 5,
+ const [webhookData, setWebhookData] = useState([]);
+ const [paginationProps, setPaginationProps] = useState({
+ limit: 10,
page: 1,
offset: 0,
total: 0,
maxPages: 1,
});
- const getMaxPages = (pagination: paginationPropTypes) => {
+
+ const getMaxPages = (pagination: PaginationProps) => {
const { limit, total } = pagination;
if (total > 1) {
return total % limit === 0
? total / limit
- : parseInt(`${total / limit}`) + 1;
- } else return 1;
+ : Math.floor(total / limit) + 1;
+ }
+ return 1;
};
+
const fetchWebookData = async () => {
setLoading(true);
const res = await client
- .query(WebhooksDataQuery, {
+ .query(WebhooksDataQuery, {
params: {
pagination: {
limit: paginationProps.limit,
@@ -94,11 +85,15 @@ const Webhooks = () => {
})
.toPromise();
if (res.data?._webhooks) {
- const { pagination, webhooks } = res.data?._webhooks;
- const maxPages = getMaxPages(pagination);
+ const { pagination, webhooks } = res.data._webhooks;
+ const maxPages = getMaxPages(pagination as unknown as PaginationProps);
if (webhooks?.length) {
setWebhookData(webhooks);
- setPaginationProps({ ...paginationProps, ...pagination, maxPages });
+ setPaginationProps({
+ ...paginationProps,
+ ...pagination,
+ maxPages,
+ });
} else {
if (paginationProps.page !== 1) {
setPaginationProps({
@@ -112,262 +107,208 @@ const Webhooks = () => {
}
setLoading(false);
};
+
const paginationHandler = (value: Record) => {
setPaginationProps({ ...paginationProps, ...value });
};
+
useEffect(() => {
fetchWebookData();
}, [paginationProps.page, paginationProps.limit]);
+
return (
-
-
-
- Webhooks
-
+
+
+
+ Webhooks
+
+ Configure webhook endpoints for user events.
+
+
-
- {!loading ? (
- webhookData.length ? (
-
-
-
- | Event Name |
- Event Description |
- Endpoint |
- Enabled |
- Headers |
- Actions |
-
-
-
- {webhookData.map((webhook: webhookDataTypes) => (
-
- |
- {webhook[WebhookInputDataFields.EVENT_NAME].split('-')[0]}
- |
-
- {webhook[WebhookInputDataFields.EVENT_DESCRIPTION]}
- |
- {webhook[WebhookInputDataFields.ENDPOINT]} |
-
-
- {webhook[WebhookInputDataFields.ENABLED].toString()}
-
- |
-
-
-
- {Object.keys(
- webhook[WebhookInputDataFields.HEADERS] || {},
- )?.length.toString()}
-
+
+ {loading ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : webhookData.length ? (
+ <>
+
+
+
+ Event Name
+ Event Description
+ Endpoint
+ Enabled
+ Headers
+ Actions
+
+
+
+ {webhookData.map((webhook) => (
+
+
+ {webhook.event_name.split('-')[0]}
+
+
+ {webhook.event_description}
+
+ {webhook.endpoint}
+
+
+ {webhook.enabled.toString()}
+
+
+
+
+
+
+ {Object.keys(webhook.headers || {}).length.toString()}
+
+
+
+
+ {JSON.stringify(webhook.headers, null, 2)}
+
+
-
-
-
- |
-
+
+
+
+
))}
-
- {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
-
-
-
-
-
- paginationHandler({
- page: 1,
- })
- }
- isDisabled={paginationProps.page <= 1}
- mr={4}
- icon={}
- />
-
-
-
- paginationHandler({
- page: paginationProps.page - 1,
- })
- }
- isDisabled={paginationProps.page <= 1}
- icon={}
- />
-
-
-
-
- Page{' '}
-
- {paginationProps.page}
- {' '}
- of{' '}
-
- {paginationProps.maxPages}
-
-
-
- Go to page:{' '}
-
- paginationHandler({
- page: parseInt(value),
- })
- }
- value={paginationProps.page}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- paginationHandler({
- page: paginationProps.page + 1,
- })
- }
- isDisabled={
- paginationProps.page >= paginationProps.maxPages
- }
- icon={}
- />
-
-
-
- paginationHandler({
- page: paginationProps.maxPages,
- })
- }
- isDisabled={
- paginationProps.page >= paginationProps.maxPages
- }
- ml={4}
- icon={}
- />
-
-
-
-
- )}
+
- ) : (
-
-
-
-
-
- No Data
-
-
- )
+
+ {/* Pagination */}
+ {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
+
+
+
+
+
+
+
+
+ Page {paginationProps.page} of{' '}
+ {paginationProps.maxPages}
+
+
+ Go to:
+
+ paginationHandler({
+ page: parseInt(e.target.value) || 1,
+ })
+ }
+ className="h-8 w-16"
+ />
+
+
+
+
+
+
+
+
+
+ )}
+ >
) : (
-
-
-
+
)}
-
+
);
};
diff --git a/web/dashboard/src/routes/index.tsx b/web/dashboard/src/routes/index.tsx
index 801f00904..4116986fa 100644
--- a/web/dashboard/src/routes/index.tsx
+++ b/web/dashboard/src/routes/index.tsx
@@ -3,12 +3,13 @@ import { Outlet, Route, Routes } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthContext';
import { DashboardLayout } from '../layouts/DashboardLayout';
-import EmailTemplates from '../pages/EmailTemplates';
const Auth = lazy(() => import('../pages/Auth'));
-const Home = lazy(() => import('../pages/Home'));
+const Overview = lazy(() => import('../pages/Overview'));
const Users = lazy(() => import('../pages/Users'));
const Webhooks = lazy(() => import('../pages/Webhooks'));
+const EmailTemplates = lazy(() => import('../pages/EmailTemplates'));
+const AuditLogs = lazy(() => import('../pages/AuditLogs'));
export const AppRoutes = () => {
const { isLoggedIn } = useAuthContext();
@@ -25,10 +26,12 @@ export const AppRoutes = () => {
}
>
- } />
+ } />
+ } />
} />
} />
- } />
+ } />
+ } />
diff --git a/web/dashboard/src/types.ts b/web/dashboard/src/types.ts
new file mode 100644
index 000000000..0974ec229
--- /dev/null
+++ b/web/dashboard/src/types.ts
@@ -0,0 +1,122 @@
+export interface User {
+ id: string;
+ email: string;
+ email_verified: boolean;
+ given_name?: string;
+ family_name?: string;
+ middle_name?: string;
+ nickname?: string;
+ gender?: string;
+ birthdate?: string;
+ phone_number?: string;
+ phone_number_verified?: boolean;
+ picture?: string;
+ signup_methods: string;
+ roles: string[];
+ created_at: number;
+ updated_at?: number;
+ revoked_timestamp?: number;
+ is_multi_factor_auth_enabled?: boolean;
+ preferred_username?: string;
+}
+
+export interface Webhook {
+ id: string;
+ event_name: string;
+ event_description?: string;
+ endpoint: string;
+ enabled: boolean;
+ headers?: Record;
+}
+
+export interface WebhookLog {
+ id: string;
+ http_status: number;
+ request: string;
+ response: string;
+ webhook_id: string;
+ created_at: number;
+}
+
+export interface EmailTemplate {
+ id: string;
+ event_name: string;
+ subject: string;
+ template: string;
+ design: string;
+ created_at: number;
+ updated_at?: number;
+}
+
+export interface AuditLog {
+ id: string;
+ actor_id: string;
+ actor_type: string;
+ actor_email: string;
+ action: string;
+ resource_type: string;
+ resource_id: string;
+ ip_address: string;
+ user_agent: string;
+ metadata: string;
+ created_at: number;
+}
+
+export interface PaginationInfo {
+ offset: number;
+ total: number;
+ page: number;
+ limit: number;
+}
+
+export interface Pagination {
+ pagination: PaginationInfo;
+}
+
+export interface UsersResponse {
+ _users: {
+ pagination: PaginationInfo;
+ users: User[];
+ };
+}
+
+export interface WebhooksResponse {
+ _webhooks: {
+ pagination: PaginationInfo;
+ webhooks: Webhook[];
+ };
+}
+
+export interface EmailTemplatesResponse {
+ _email_templates: {
+ pagination: PaginationInfo;
+ email_templates: EmailTemplate[];
+ };
+}
+
+export interface WebhookLogsResponse {
+ _webhook_logs: {
+ pagination: PaginationInfo;
+ webhook_logs: WebhookLog[];
+ };
+}
+
+export interface AuditLogsResponse {
+ _audit_logs: {
+ pagination: PaginationInfo;
+ audit_logs: AuditLog[];
+ };
+}
+
+export interface MetaResponse {
+ meta: {
+ version: string;
+ client_id: string;
+ };
+}
+
+export interface AdminSessionResponse {
+ _admin_session: {
+ message: string;
+ };
+}
diff --git a/web/dashboard/src/utils/index.ts b/web/dashboard/src/utils/index.ts
index c45f5414f..46b3192db 100644
--- a/web/dashboard/src/utils/index.ts
+++ b/web/dashboard/src/utils/index.ts
@@ -1,7 +1,16 @@
import _ from 'lodash';
+interface AuthorizerWindow extends Window {
+ __authorizer__: {
+ isOnboardingCompleted: boolean;
+ };
+}
+
export const hasAdminSecret = () => {
- return (window)['__authorizer__'].isOnboardingCompleted === true;
+ return (
+ (window as unknown as AuthorizerWindow).__authorizer__
+ .isOnboardingCompleted === true
+ );
};
export const capitalizeFirstLetter = (data: string): string =>
@@ -20,9 +29,7 @@ const fallbackCopyTextToClipboard = (text: string) => {
textArea.select();
try {
- const successful = document.execCommand('copy');
- const msg = successful ? 'successful' : 'unsuccessful';
- console.log('Fallback: Copying text command was ' + msg);
+ document.execCommand('copy');
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
@@ -35,26 +42,31 @@ export const copyTextToClipboard = async (text: string) => {
return;
}
try {
- navigator.clipboard.writeText(text);
+ await navigator.clipboard.writeText(text);
} catch (err) {
throw err;
}
};
-export const getObjectDiff = (obj1: any, obj2: any) => {
+export const getObjectDiff = (
+ obj1: Record,
+ obj2: Record,
+): string[] => {
const diff = Object.keys(obj1).reduce((result, key) => {
- if (!obj2.hasOwnProperty(key)) {
+ if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
result.push(key);
} else if (
_.isEqual(obj1[key], obj2[key]) ||
(obj1[key] === null && obj2[key] === '') ||
(obj1[key] &&
Array.isArray(obj1[key]) &&
- obj1[key].length === 0 &&
+ (obj1[key] as unknown[]).length === 0 &&
obj2[key] === null)
) {
const resultKeyIndex = result.indexOf(key);
- result.splice(resultKeyIndex, 1);
+ if (resultKeyIndex >= 0) {
+ result.splice(resultKeyIndex, 1);
+ }
}
return result;
}, Object.keys(obj2));
diff --git a/web/dashboard/src/utils/parseCSV.ts b/web/dashboard/src/utils/parseCSV.ts
index 6006b5d2f..8d8f3ccfe 100644
--- a/web/dashboard/src/utils/parseCSV.ts
+++ b/web/dashboard/src/utils/parseCSV.ts
@@ -1,28 +1,21 @@
import _flatten from 'lodash/flatten';
import { validateEmail } from '.';
-interface dataTypes {
+interface DataTypes {
value: string;
isInvalid: boolean;
}
-const parseCSV = (file: File, delimiter: string): Promise => {
+const parseCSV = (file: File, delimiter: string): Promise => {
return new Promise((resolve) => {
const reader = new FileReader();
- // When the FileReader has loaded the file...
- reader.onload = (e: any) => {
- // Split the result to an array of lines
- const lines = e.target.result.split('\n');
- // Split the lines themselves by the specified
- // delimiter, such as a comma
+ reader.onload = (e: ProgressEvent) => {
+ const lines = (e.target?.result as string).split('\n');
let result = lines.map((line: string) => line.split(delimiter));
- // As the FileReader reads asynchronously,
- // we can't just return the result; instead,
- // we're passing it to a callback function
- result = _flatten(result);
+ const flattened = _flatten(result);
resolve(
- result.map((email: string) => {
+ flattened.map((email: string) => {
return {
value: email.trim(),
isInvalid: !validateEmail(email.trim()),
@@ -31,7 +24,6 @@ const parseCSV = (file: File, delimiter: string): Promise => {
);
};
- // Read the file content as a single string
reader.readAsText(file);
});
};
diff --git a/web/dashboard/vite.config.ts b/web/dashboard/vite.config.ts
index 9a0d4e2f9..45da72d75 100644
--- a/web/dashboard/vite.config.ts
+++ b/web/dashboard/vite.config.ts
@@ -1,9 +1,10 @@
import { defineConfig, type PluginOption } from 'vite';
import react from '@vitejs/plugin-react';
+import tailwindcss from '@tailwindcss/vite';
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()] as PluginOption[],
+ plugins: [react(), tailwindcss()] as PluginOption[],
build: {
outDir: 'build',
emptyOutDir: true,
@@ -15,11 +16,14 @@ export default defineConfig({
entryFileNames: 'index.js',
chunkFileNames: 'chunk-[name]-[hash].js',
assetFileNames: (assetInfo) => {
+ if (assetInfo.names?.some((name) => name.endsWith('.css'))) {
+ return '[name][extname]';
+ }
return 'assets/[name]-[hash][extname]';
},
},
},
},
- base: '/dashboard/',
+ base: '/dashboard/build/',
publicDir: 'public',
});
diff --git a/web/templates/dashboard.tmpl b/web/templates/dashboard.tmpl
index a3e4ab4a2..4c224fc5d 100644
--- a/web/templates/dashboard.tmpl
+++ b/web/templates/dashboard.tmpl
@@ -8,900 +8,10 @@
+
-
| |