Backend Dashboard
The Backend Dashboard is the administrative interface for the Heimdall platform, built with Next.js 15 and the App Router.
Features
Dashboard
The main dashboard provides an overview of platform statistics and recent activity:
- Active connections count
- GPS data points
- User statistics
- Recent activity feed
GPS Management
View and manage GPS tracking data:
- Real-time GPS position display
- Historical GPS data table with pagination
- GPS data export functionality
- Interactive map visualization
User Management
Manage platform users (Admin only):
- View all registered users
- Assign roles and permissions
- View user activity
- Suspend/ban users
Developer Tools
Tools for developers integrating with Heimdall:
- OAuth application management
- Create/edit OAuth apps
- View client credentials
- Manage redirect URIs
Pages & Routes
| Route | Description | Auth Required |
|---|---|---|
/ | Landing page | No |
/auth/login | Login page | No |
/auth/error | Auth error page | No |
/dashboard | Main dashboard | Yes |
/developer/oauth-apps | OAuth app list | Yes |
/developer/oauth-apps/new | Create OAuth app | Yes |
/developer/oauth-apps/[id] | Edit OAuth app | Yes |
Authentication
The Backend Dashboard uses NextAuth with a custom Heimdall provider:
// src/lib/auth/heimdall-provider.ts
import { OAuthConfig } from "next-auth/providers";
export const HeimdallProvider: OAuthConfig<Profile> = {
id: "heimdall",
name: "Heimdall",
type: "oauth",
authorization: {
url: `${process.env.HEIMDALL_ID_URL}/oauth/authorize`,
params: { scope: "openid profile email" }
},
token: `${process.env.API_URL}/v1/oauth/token`,
userinfo: `${process.env.API_URL}/v1/oauth/userinfo`,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture
};
}
};
Session Handling
Sessions are managed via NextAuth with JWT strategy:
// middleware.ts
import { auth } from "@/lib/auth";
export default auth((req) => {
if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
const newUrl = new URL("/auth/login", req.nextUrl.origin);
newUrl.searchParams.set("callbackUrl", req.nextUrl.pathname);
return Response.redirect(newUrl);
}
});
export const config = {
matcher: ["/dashboard/:path*", "/developer/:path*"]
};
Components
Protected Route
Wrap pages that require authentication:
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
export default function DashboardPage() {
return (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
);
}
Protected Content
Conditionally render content based on authentication:
import { ProtectedContent } from "@/components/auth/ProtectedContent";
export default function Page() {
return (
<ProtectedContent
fallback={<LoginPrompt />}
requiredPermissions={["gps:read"]}
>
<GpsDataTable />
</ProtectedContent>
);
}
User Button
Display user info and logout:
import { UserButton } from "@/components/auth/UserButton";
export default function Header() {
return (
<nav>
<UserButton />
</nav>
);
}
Hooks
useAuth
Access authentication state:
import { useAuth } from "@/hooks/useAuth";
function Component() {
const { user, isLoading, isAuthenticated } = useAuth();
if (isLoading) return <Spinner />;
if (!isAuthenticated) return <LoginPrompt />;
return <div>Welcome, {user.name}</div>;
}
usePermissions
Check user permissions:
import { usePermissions } from "@/hooks/usePermissions";
function Component() {
const { permissions, hasPermission, isLoading } = usePermissions();
if (hasPermission("gps:write")) {
return <CreateGpsButton />;
}
return null;
}
API Integration
GraphQL Client
The dashboard uses Apollo Client for GraphQL:
// src/components/providers/ApolloWrapper.tsx
"use client";
import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client";
import { useSession } from "next-auth/react";
export function ApolloWrapper({ children }) {
const { data: session } = useSession();
const client = new ApolloClient({
uri: `${process.env.NEXT_PUBLIC_API_URL}/v1/gql`,
cache: new InMemoryCache(),
headers: {
Authorization: session?.accessToken
? `Bearer ${session.accessToken}`
: ""
}
});
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
Example Query
import { gql, useQuery } from "@apollo/client";
const GET_GPS_DATA = gql`
query GetGpsData($page: Int!, $limit: Int!) {
gpsList(page: $page, limit: $limit) {
data {
id
latitude
longitude
altitude
timestamp
speed
}
pagination {
page
limit
total
totalPages
}
}
}
`;
function GpsDataTable() {
const { data, loading, error } = useQuery(GET_GPS_DATA, {
variables: { page: 1, limit: 10 }
});
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<table>
{data.gpsList.data.map((gps) => (
<tr key={gps.id}>
<td>{gps.latitude}</td>
<td>{gps.longitude}</td>
<td>{gps.speed}</td>
</tr>
))}
</table>
);
}
Layouts
Dashboard Layout
The main layout for authenticated pages:
// src/app/dashboard/layout.tsx
import { DashboardLayout } from "@/components/layouts/DashboardLayout";
export default function Layout({ children }) {
return <DashboardLayout>{children}</DashboardLayout>;
}
Features:
- Sidebar navigation
- Header with user menu
- Breadcrumbs
- Footer
Overlay Layout
Full-screen overlay layout for modals:
import { OverlayLayout } from "@/layouts/OverlayLayout";
export default function ModalPage() {
return (
<OverlayLayout>
<Modal />
</OverlayLayout>
);
}
UI Components
The dashboard includes a set of reusable UI components:
Button
import { Button } from "@/components/ui/Button";
<Button variant="primary" size="lg" onClick={handleClick}>
Click Me
</Button>
<Button variant="outline" disabled>
Disabled
</Button>
Alert
import { Alert } from "@/components/ui/Alert";
<Alert type="success" title="Success!">
Your changes have been saved.
</Alert>
<Alert type="error" title="Error">
Something went wrong.
</Alert>
Modal
import { Modal } from "@/components/ui/Modal";
<Modal isOpen={isOpen} onClose={handleClose} title="Confirm Action">
<p>Are you sure you want to proceed?</p>
<div>
<Button onClick={handleClose}>Cancel</Button>
<Button variant="primary" onClick={handleConfirm}>
Confirm
</Button>
</div>
</Modal>
Loading Spinner
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
<LoadingSpinner size="lg" />
Floating Input
import { FloatingInput } from "@/components/ui/FloatingInput";
<FloatingInput
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
Environment Variables
| Variable | Description | Required |
|---|---|---|
NEXTAUTH_URL | The base URL of the app | Yes |
NEXTAUTH_SECRET | Secret for signing tokens | Yes |
NEXT_PUBLIC_API_URL | Public API URL | Yes |
API_URL | Server-side API URL | Yes |
HEIMDALL_ID_URL | Heimdall ID service URL | Yes |
Development
cd platform/backend
# Install dependencies
pnpm install
# Run development server
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Run linting
pnpm lint
# Run E2E tests
pnpm test:e2e
Next Steps
- Heimdall ID - Identity service documentation
- Components - Shared component documentation
- Authentication Flow - How auth works