upd: auth
Deskripsi: -update login - update struktur database No Issues
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box, useComputedColorScheme } from '@mantine/core'
|
||||
import { Avatar, Button, Card, Group, Stack, Text, useComputedColorScheme } from '@mantine/core'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb'
|
||||
import { TbChevronRight, TbDeviceMobile } from 'react-icons/tb'
|
||||
|
||||
interface AppCardProps {
|
||||
id: string
|
||||
name: string
|
||||
status: 'active' | 'warning' | 'error'
|
||||
users: number
|
||||
users?: number
|
||||
errors: number
|
||||
version: string
|
||||
maintenance?: boolean
|
||||
}
|
||||
|
||||
export function AppCard({ id, name, status, users, errors, version }: AppCardProps) {
|
||||
export function AppCard({ id, name, status, errors, version }: AppCardProps) {
|
||||
const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red'
|
||||
const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
|
||||
|
||||
@@ -46,12 +47,12 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
||||
</Avatar>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="lg" style={{ letterSpacing: '-0.3px' }}>{name}</Text>
|
||||
<Text size="xs" c="dimmed" fw={600}>VERSION {version}</Text>
|
||||
{/* <Text size="xs" c="dimmed" fw={600}>VERSION {version}</Text> */}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge color={statusColor} variant="dot" size="sm">
|
||||
{/* <Badge color={statusColor} variant="dot" size="sm">
|
||||
{status.toUpperCase()}
|
||||
</Badge>
|
||||
</Badge> */}
|
||||
</Group>
|
||||
|
||||
{/* <Stack gap="md" mt="sm">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { APP_CONFIGS } from '@/frontend/config/appMenus'
|
||||
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
|
||||
import {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
Burger,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Menu,
|
||||
NavLink,
|
||||
Select,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
useMantineColorScheme
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
|
||||
import {
|
||||
TbAlertTriangle,
|
||||
@@ -50,6 +53,26 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const matches = useMatches()
|
||||
const currentPath = matches[matches.length - 1]?.pathname
|
||||
|
||||
// ─── Connect to auth system ──────────────────────────
|
||||
const { data: sessionData } = useSession()
|
||||
const user = sessionData?.user
|
||||
const logout = useLogout()
|
||||
|
||||
// ─── Fetch registered apps from database ─────────────
|
||||
const { data: appsData } = useQuery({
|
||||
queryKey: ['apps'],
|
||||
queryFn: () => fetch('/api/apps', { credentials: 'include' }).then((r) => r.json()),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
// ─── Fetch system status from database ───────────────
|
||||
const { data: systemStatus } = useQuery({
|
||||
queryKey: ['system', 'status'],
|
||||
queryFn: () => fetch('/api/system/status', { credentials: 'include' }).then((r) => r.json()),
|
||||
refetchInterval: 30_000, // refresh every 30 seconds
|
||||
staleTime: 15_000,
|
||||
})
|
||||
|
||||
const globalNav = [
|
||||
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
|
||||
{ label: 'Applications', icon: TbApps, to: '/apps' },
|
||||
@@ -61,6 +84,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const activeApp = appId ? APP_CONFIGS[appId] : null
|
||||
const navLinks = activeApp ? activeApp.menus : globalNav
|
||||
|
||||
// Build app selector data from API
|
||||
const appSelectData = (appsData || []).map((app: any) => ({
|
||||
value: app.id,
|
||||
label: app.name,
|
||||
}))
|
||||
|
||||
// System status indicator
|
||||
const isOperational = systemStatus?.status === 'operational'
|
||||
const statusColor = isOperational ? '#10b981' : '#f59e0b'
|
||||
const statusText = isOperational ? 'All Systems Operational' : 'System Degraded'
|
||||
|
||||
const handleLogout = () => {
|
||||
logout.mutate()
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 70 }}
|
||||
@@ -115,21 +153,47 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<Menu.Target>
|
||||
<Avatar
|
||||
src={undefined}
|
||||
alt="User"
|
||||
alt={user?.name || 'User'}
|
||||
color="brand-blue"
|
||||
radius="xl"
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
>
|
||||
{user?.name?.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
{user && (
|
||||
<>
|
||||
<Menu.Label>
|
||||
<Text size="sm" fw={600} truncate>{user.name}</Text>
|
||||
<Text size="xs" c="dimmed" truncate>{user.email}</Text>
|
||||
</Menu.Label>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<Menu.Label>Application</Menu.Label>
|
||||
<Menu.Item leftSection={<TbUserCircle size={16} />}>Profile</Menu.Item>
|
||||
<Menu.Item leftSection={<TbSettings size={16} />}>Settings</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<TbUserCircle size={16} />}
|
||||
onClick={() => navigate({ to: '/profile' })}
|
||||
>
|
||||
Profile
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<TbSettings size={16} />}
|
||||
onClick={() => navigate({ to: '/dashboard' })}
|
||||
>
|
||||
Settings
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Label>Danger Zone</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<TbLogout size={16} />}>
|
||||
Logout
|
||||
<Menu.Item
|
||||
color="red"
|
||||
leftSection={<TbLogout size={16} />}
|
||||
onClick={handleLogout}
|
||||
disabled={logout.isPending}
|
||||
>
|
||||
{logout.isPending ? 'Logging out...' : 'Logout'}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
@@ -160,10 +224,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<Select
|
||||
label="Selected Application"
|
||||
value={appId}
|
||||
data={[
|
||||
data={appSelectData.length > 0 ? appSelectData : [
|
||||
{ value: 'desa-plus', label: 'Desa+' },
|
||||
{ value: 'e-commerce', label: 'E-Commerce' },
|
||||
{ value: 'fitness-app', label: 'Fitness App' },
|
||||
]}
|
||||
onChange={(val) => val && navigate({ to: '/apps/$appId', params: { appId: val } })}
|
||||
radius="md"
|
||||
@@ -225,19 +287,26 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
>
|
||||
<Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text>
|
||||
<Group gap="xs">
|
||||
<Box style={{ width: 8, height: 8, borderRadius: '50%', background: '#10b981' }} />
|
||||
<Text size="sm" fw={500}>All Systems Operational</Text>
|
||||
<Box style={{ width: 8, height: 8, borderRadius: '50%', background: statusColor, boxShadow: `0 0 6px ${statusColor}` }} />
|
||||
<Text size="sm" fw={500}>{statusText}</Text>
|
||||
</Group>
|
||||
{systemStatus && (
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{systemStatus.activeSessions} active session{systemStatus.activeSessions !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
fullWidth
|
||||
leftSection={<TbLogout size={16} />}
|
||||
leftSection={logout.isPending ? <Loader size={16} color="red" /> : <TbLogout size={16} />}
|
||||
mt="md"
|
||||
onClick={handleLogout}
|
||||
disabled={logout.isPending}
|
||||
>
|
||||
Log out
|
||||
{logout.isPending ? 'Logging out...' : 'Log out'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
export type Role = 'USER' | 'ADMIN' | 'SUPER_ADMIN'
|
||||
export type Role = | 'ADMIN' | 'DEVELOPER'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
@@ -41,12 +41,7 @@ export function useLogin() {
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData(['auth', 'session'], data)
|
||||
// Super admin → dashboard, others → profile
|
||||
if (data.user.role === 'SUPER_ADMIN') {
|
||||
navigate({ to: '/dashboard' })
|
||||
} else {
|
||||
navigate({ to: '/profile' })
|
||||
}
|
||||
navigate({ to: '/dashboard' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Container, Stack, Title, Text, SimpleGrid, Group, Button, TextInput, Loader } from '@mantine/core'
|
||||
import { useDebouncedValue } from '@mantine/hooks'
|
||||
import { useState } from 'react'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { TbPlus, TbSearch } from 'react-icons/tb'
|
||||
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
||||
@@ -10,9 +12,12 @@ export const Route = createFileRoute('/apps/')({
|
||||
})
|
||||
|
||||
function AppsPage() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 300)
|
||||
|
||||
const { data: apps, isLoading } = useQuery({
|
||||
queryKey: ['apps'],
|
||||
queryFn: () => fetch('/api/apps').then((r) => r.json()),
|
||||
queryKey: ['apps', debouncedSearch],
|
||||
queryFn: () => fetch(`/api/apps?search=${encodeURIComponent(debouncedSearch)}`).then((r) => r.json()),
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -24,14 +29,14 @@ function AppsPage() {
|
||||
<Title order={2} className="gradient-text">Applications</Title>
|
||||
<Text size="sm" c="dimmed">Manage and monitor all your mobile applications from one place.</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="gradient"
|
||||
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
||||
leftSection={<TbPlus size={18} />}
|
||||
radius="md"
|
||||
>
|
||||
Add New Application
|
||||
</Button>
|
||||
</Button> */}
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
@@ -40,6 +45,8 @@ function AppsPage() {
|
||||
leftSection={<TbSearch size={16} />}
|
||||
style={{ flex: 1 }}
|
||||
radius="md"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ export const Route = createFileRoute('/dashboard')({
|
||||
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
|
||||
})
|
||||
if (!data?.user) throw redirect({ to: '/login' })
|
||||
if (data.user.role !== 'SUPER_ADMIN') throw redirect({ to: '/profile' })
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw redirect({ to: '/login' })
|
||||
throw e
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useLogin } from '@/frontend/hooks/useAuth'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -13,8 +14,7 @@ import {
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { FcGoogle } from 'react-icons/fc'
|
||||
import { TbAlertCircle, TbLogin, TbLock, TbMail } from 'react-icons/tb'
|
||||
import { useLogin } from '@/frontend/hooks/useAuth'
|
||||
import { TbAlertCircle, TbLock, TbLogin, TbMail } from 'react-icons/tb'
|
||||
|
||||
export const Route = createFileRoute('/login')({
|
||||
validateSearch: (search: Record<string, unknown>): { error?: string } => ({
|
||||
@@ -27,7 +27,7 @@ export const Route = createFileRoute('/login')({
|
||||
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
|
||||
})
|
||||
if (data?.user) {
|
||||
throw redirect({ to: data.user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile' })
|
||||
throw redirect({ to: '/dashboard' })
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) return
|
||||
@@ -57,12 +57,6 @@ function LoginPage() {
|
||||
Login
|
||||
</Title>
|
||||
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
Demo: <strong>superadmin@example.com</strong> / <strong>superadmin123</strong>
|
||||
<br />
|
||||
or: <strong>user@example.com</strong> / <strong>user123</strong>
|
||||
</Text>
|
||||
|
||||
{(login.isError || searchError) && (
|
||||
<Alert icon={<TbAlertCircle size={16} />} color="red" variant="light">
|
||||
{login.isError ? login.error.message : 'Google login failed, please try again.'}
|
||||
@@ -95,18 +89,6 @@ function LoginPage() {
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
<Divider label="or" labelPosition="center" />
|
||||
|
||||
<Button
|
||||
component="a"
|
||||
href="/api/auth/google"
|
||||
fullWidth
|
||||
variant="default"
|
||||
leftSection={<FcGoogle size={18} />}
|
||||
>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
@@ -30,9 +30,8 @@ export const Route = createFileRoute('/profile')({
|
||||
})
|
||||
|
||||
const roleBadgeColor: Record<string, string> = {
|
||||
USER: 'blue',
|
||||
ADMIN: 'violet',
|
||||
SUPER_ADMIN: 'red',
|
||||
DEVELOPER: 'red',
|
||||
}
|
||||
|
||||
function ProfilePage() {
|
||||
|
||||
@@ -59,20 +59,15 @@ const getRoleColor = (role: string) => {
|
||||
}
|
||||
|
||||
const roles = [
|
||||
{
|
||||
name: 'SUPER_ADMIN',
|
||||
color: 'red',
|
||||
permissions: ['Full Access', 'User Mgmt', 'Role Mgmt', 'App Config', 'Logs & Errors']
|
||||
},
|
||||
{
|
||||
name: 'DEVELOPER',
|
||||
color: 'brand-blue',
|
||||
permissions: ['View All Apps', 'Manage Assigned App', 'View Logs', 'Resolve Errors', 'Village Setup']
|
||||
color: 'red',
|
||||
permissions: ['Full Access', 'Error Feedback', 'Error Management', 'App Version Management', 'User Management']
|
||||
},
|
||||
{
|
||||
name: 'QA',
|
||||
name: 'ADMIN',
|
||||
color: 'orange',
|
||||
permissions: ['View All Apps', 'View Logs', 'Report Errors', 'Test App Features']
|
||||
permissions: ['View All Apps', 'View Logs', 'Report Errors']
|
||||
},
|
||||
]
|
||||
|
||||
@@ -414,10 +409,8 @@ function UsersPage() {
|
||||
<Select
|
||||
label="Role"
|
||||
data={[
|
||||
{ value: 'USER', label: 'User' },
|
||||
{ value: 'ADMIN', label: 'Admin' },
|
||||
{ value: 'DEVELOPER', label: 'Developer' },
|
||||
{ value: 'SUPER_ADMIN', label: 'Super Admin' },
|
||||
]}
|
||||
value={createForm.role}
|
||||
onChange={(val) => setCreateForm({ ...createForm, role: val || 'USER' })}
|
||||
@@ -461,10 +454,8 @@ function UsersPage() {
|
||||
<Select
|
||||
label="Role"
|
||||
data={[
|
||||
{ value: 'USER', label: 'User' },
|
||||
{ value: 'ADMIN', label: 'Admin' },
|
||||
{ value: 'DEVELOPER', label: 'Developer' },
|
||||
{ value: 'SUPER_ADMIN', label: 'Super Admin' },
|
||||
]}
|
||||
value={editForm.role}
|
||||
onChange={(val) => setEditForm({ ...editForm, role: val || 'USER' })}
|
||||
|
||||
Reference in New Issue
Block a user