remove old frontend

This commit is contained in:
2025-08-27 13:10:11 -04:00
parent 500f2c2d9c
commit f72c05bfd8
37 changed files with 0 additions and 38751 deletions

View File

@ -1,8 +0,0 @@
# KMS Frontend Configuration
REACT_APP_API_URL=http://localhost:8080
REACT_APP_APP_NAME=KMS Frontend
REACT_APP_VERSION=1.0.0
# Development settings
GENERATE_SOURCEMAP=true
SKIP_PREFLIGHT_CHECK=true

View File

@ -1,24 +0,0 @@
dist
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,30 +0,0 @@
# Multi-stage build for React frontend
FROM node:24-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage with nginx
FROM docker.io/library/nginx:alpine
# Copy built application
COPY --from=builder /app/build /usr/share/nginx/html
# Copy custom nginx config if needed
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,213 +0,0 @@
# KMS Frontend - Key Management System
A modern React frontend for the Key Management System (KMS) API, built with TypeScript and Ant Design.
## Features
- **Dashboard**: System overview with health status and statistics
- **Application Management**: Create, view, edit, and delete applications
- **Token Management**: Create and manage static tokens with permissions
- **User Management**: Handle user authentication and token operations
- **Audit Logging**: Monitor system activities and security events
- **Responsive Design**: Mobile-friendly interface with Ant Design components
## Technology Stack
- **React 18** with TypeScript
- **Ant Design** for UI components
- **Axios** for API communication
- **React Router** for navigation
- **Day.js** for date handling
## Getting Started
### Prerequisites
- Node.js (v14 or higher)
- npm or yarn
- KMS API server running on `http://localhost:8080`
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd kms-frontend
```
2. Install dependencies:
```bash
npm install
```
3. Configure environment variables:
```bash
cp .env.example .env
```
Edit `.env` to match your API configuration:
```
REACT_APP_API_URL=http://localhost:8080
REACT_APP_APP_NAME=KMS Frontend
REACT_APP_VERSION=1.0.0
```
4. Start the development server:
```bash
npm start
```
The application will be available at `http://localhost:3000`.
## Project Structure
```
src/
├── components/ # React components
│ ├── Applications.tsx # Application management
│ ├── Audit.tsx # Audit logging
│ ├── Dashboard.tsx # Main dashboard
│ ├── Login.tsx # Authentication
│ ├── Tokens.tsx # Token management
│ └── Users.tsx # User management
├── contexts/ # React contexts
│ └── AuthContext.tsx # Authentication context
├── services/ # API services
│ └── apiService.ts # KMS API client
├── App.tsx # Main application component
├── App.css # Custom styles
└── index.tsx # Application entry point
```
## API Integration
The frontend integrates with the KMS API and supports:
- **Health Checks**: Monitor API availability
- **Application CRUD**: Full application lifecycle management
- **Token Operations**: Create, verify, and manage tokens
- **User Authentication**: Login and token renewal flows
- **Audit Trails**: View system activity logs
## Authentication
The application uses a demo authentication system for development:
1. Enter any valid email address on the login screen
2. The system will simulate authentication and grant permissions
3. In production, integrate with your identity provider (OAuth2, SAML, etc.)
## Available Scripts
- `npm start` - Start development server
- `npm run build` - Build for production
- `npm test` - Run tests
- `npm run eject` - Eject from Create React App
## Configuration
### Environment Variables
- `REACT_APP_API_URL` - KMS API base URL
- `REACT_APP_APP_NAME` - Application name
- `REACT_APP_VERSION` - Application version
### API Configuration
The API service automatically includes the `X-User-Email` header for authentication. Configure your KMS API to accept this header for demo purposes.
## Features Overview
### Dashboard
- System health monitoring
- Application and token statistics
- Quick action shortcuts
### Applications
- Create new applications with configuration
- View application details and security settings
- Edit application properties
- Delete applications and associated tokens
### Tokens
- Create static tokens with specific permissions
- View token details and metadata
- Verify token validity and permissions
- Delete tokens when no longer needed
### Users
- Current user information display
- Initiate user authentication flows
- Renew user tokens
- Authentication method documentation
### Audit Log
- Comprehensive activity tracking
- Filter by date, user, action, and status
- Detailed event information
- Timeline view of recent activities
## Security Considerations
- All API communications should use HTTPS in production
- Tokens are displayed only once during creation
- Sensitive information is masked in the UI
- Audit logging tracks all user actions
## Development
### Adding New Features
1. Create new components in `src/components/`
2. Add API methods to `src/services/apiService.ts`
3. Update routing in `src/App.tsx`
4. Add navigation items to the sidebar menu
### Styling
The application uses Ant Design's theming system with custom CSS overrides in `App.css`. Follow Ant Design's design principles for consistency.
### Testing
Run the test suite with:
```bash
npm test
```
## Production Deployment
1. Build the application:
```bash
npm run build
```
2. Deploy the `build/` directory to your web server
3. Configure your web server to serve the React app
4. Ensure the KMS API is accessible from your production domain
5. Update environment variables for production
## Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## License
This project is licensed under the MIT License.
## Support
For issues and questions:
- Check the KMS API documentation
- Review the Ant Design documentation
- Create an issue in the repository

View File

@ -1,50 +0,0 @@
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
devServer: {
port: 3001,
historyApiFallback: true,
},
webpack: {
plugins: {
add: [
new ModuleFederationPlugin({
name: 'kms',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/federated/KMSApp',
'./SearchProvider': './src/federated/SearchProvider',
},
shared: {
react: {
singleton: true,
requiredVersion: '^19.1.1'
},
'react-dom': {
singleton: true,
requiredVersion: '^19.1.1'
},
'react-router-dom': {
singleton: true,
requiredVersion: '^7.8.2'
},
antd: {
singleton: true,
requiredVersion: '^5.27.1'
},
axios: {
singleton: true
}
},
}),
],
},
configure: (webpackConfig) => ({
...webpackConfig,
output: {
...webpackConfig.output,
publicPath: "auto",
},
}),
},
};

View File

@ -1,38 +0,0 @@
error_log /var/log/nginx/error.log warn;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Handle React Router (client-side routing)
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
application/javascript
application/json
text/css
text/javascript
text/plain
text/xml;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
{
"name": "kms-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@types/react-router-dom": "^5.3.3",
"antd": "^5.27.1",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"craco-module-federation": "^1.1.0",
"webpack": "^5.96.1"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,262 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
padding: 20px;
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Custom KMS Frontend Styles */
.demo-logo-vertical {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
border-radius: 6px;
}
.ant-layout-sider-collapsed .demo-logo-vertical {
margin: 16px 8px;
}
/* Custom card hover effects */
.ant-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s ease;
}
/* Custom table styles */
.ant-table-thead > tr > th {
background-color: #fafafa;
font-weight: 600;
}
/* Custom form styles */
.ant-form-item-label > label {
font-weight: 500;
}
/* Custom button styles */
.ant-btn-primary {
border-radius: 6px;
}
.ant-btn {
border-radius: 6px;
}
/* Custom tag styles */
.ant-tag {
border-radius: 4px;
font-weight: 500;
}
/* Custom modal styles */
.ant-modal-header {
border-radius: 8px 8px 0 0;
}
.ant-modal-content {
border-radius: 8px;
}
/* Custom alert styles */
.ant-alert {
border-radius: 6px;
}
/* Custom timeline styles */
.ant-timeline-item-content {
margin-left: 8px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.ant-layout-sider {
position: fixed !important;
height: 100vh;
z-index: 999;
}
.ant-layout-content {
margin-left: 0 !important;
}
}
/* Loading spinner customization */
.ant-spin-dot-item {
background-color: #1890ff;
}
/* Custom scrollbar for code blocks */
pre::-webkit-scrollbar {
width: 6px;
height: 6px;
}
pre::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
pre::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
pre::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Custom input styles */
.ant-input, .ant-input-affix-wrapper {
border-radius: 6px;
}
.ant-select-selector {
border-radius: 6px !important;
}
/* Custom statistic styles */
.ant-statistic-content {
font-weight: 600;
}
/* Custom menu styles */
.ant-menu-dark .ant-menu-item-selected {
background-color: #1890ff;
}
.ant-menu-dark .ant-menu-item:hover {
background-color: rgba(24, 144, 255, 0.2);
}
/* Custom pagination styles */
.ant-pagination-item-active {
border-color: #1890ff;
}
.ant-pagination-item-active a {
color: #1890ff;
}
/* Custom drawer styles for mobile */
@media (max-width: 768px) {
.ant-drawer-content-wrapper {
width: 280px !important;
}
}
/* Custom notification styles */
.ant-notification {
border-radius: 8px;
}
/* Custom tooltip styles */
.ant-tooltip-inner {
border-radius: 6px;
}
/* Custom progress styles */
.ant-progress-bg {
border-radius: 4px;
}
/* Custom switch styles */
.ant-switch {
border-radius: 12px;
}
/* Custom checkbox styles */
.ant-checkbox-wrapper {
font-weight: 500;
}
/* Custom radio styles */
.ant-radio-wrapper {
font-weight: 500;
}
/* Custom date picker styles */
.ant-picker {
border-radius: 6px;
}
/* Custom upload styles */
.ant-upload {
border-radius: 6px;
}
/* Custom collapse styles */
.ant-collapse {
border-radius: 6px;
}
.ant-collapse-item {
border-radius: 6px;
}
/* Custom tabs styles */
.ant-tabs-tab {
font-weight: 500;
}
/* Custom steps styles */
.ant-steps-item-title {
font-weight: 600;
}
/* Custom breadcrumb styles */
.ant-breadcrumb {
font-weight: 500;
}
/* Custom anchor styles */
.ant-anchor-link-title {
font-weight: 500;
}
/* Custom back-top styles */
.ant-back-top {
border-radius: 20px;
}
/* Custom result styles */
.ant-result-title {
font-weight: 600;
}
/* Custom empty styles */
.ant-empty-description {
font-weight: 500;
}
/* Custom spin styles */
.ant-spin-text {
font-weight: 500;
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,142 +0,0 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { ConfigProvider, Layout, Menu, theme } from 'antd';
import {
DashboardOutlined,
AppstoreOutlined,
KeyOutlined,
UserOutlined,
AuditOutlined,
LoginOutlined,
ExperimentOutlined,
} from '@ant-design/icons';
import { useState } from 'react';
import './App.css';
// Components
import Dashboard from './components/Dashboard';
import Applications from './components/Applications';
import Tokens from './components/Tokens';
import Users from './components/Users';
import Audit from './components/Audit';
import Login from './components/Login';
import TokenTester from './components/TokenTester';
import TokenTesterCallback from './components/TokenTesterCallback';
import { AuthProvider, useAuth } from './contexts/AuthContext';
const { Header, Sider, Content } = Layout;
const AppContent: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const { user, logout } = useAuth();
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
if (!user) {
return <Login />;
}
const menuItems = [
{
key: '/',
icon: <DashboardOutlined />,
label: 'Dashboard',
},
{
key: '/applications',
icon: <AppstoreOutlined />,
label: 'Applications',
},
{
key: '/tokens',
icon: <KeyOutlined />,
label: 'Tokens',
},
{
key: '/token-tester',
icon: <ExperimentOutlined />,
label: 'Token Tester',
},
{
key: '/users',
icon: <UserOutlined />,
label: 'Users',
},
{
key: '/audit',
icon: <AuditOutlined />,
label: 'Audit Log',
},
];
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="demo-logo-vertical" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['/']}
items={menuItems}
onClick={({ key }) => {
window.location.pathname = key;
}}
/>
</Sider>
<Layout>
<Header style={{ padding: 0, background: colorBgContainer, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ paddingLeft: 16, fontSize: '18px', fontWeight: 'bold' }}>
KMS - Key Management System
</div>
<div style={{ paddingRight: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
<span>Welcome, {user.email}</span>
<LoginOutlined
onClick={logout}
style={{ cursor: 'pointer', fontSize: '16px' }}
title="Logout"
/>
</div>
</Header>
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
borderRadius: borderRadiusLG,
}}
>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/applications" element={<Applications />} />
<Route path="/tokens" element={<Tokens />} />
<Route path="/token-tester" element={<TokenTester />} />
<Route path="/token-tester/callback" element={<TokenTesterCallback />} />
<Route path="/users" element={<Users />} />
<Route path="/audit" element={<Audit />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Content>
</Layout>
</Layout>
);
};
const App: React.FC = () => {
return (
<ConfigProvider
theme={{
algorithm: theme.defaultAlgorithm,
}}
>
<AuthProvider>
<Router>
<AppContent />
</Router>
</AuthProvider>
</ConfigProvider>
);
};
export default App;

View File

@ -1,24 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import { ConfigProvider, theme } from 'antd';
import App from './App';
import './App.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<ConfigProvider
theme={{
algorithm: theme.defaultAlgorithm,
}}
>
<Router>
<App />
</Router>
</ConfigProvider>
</React.StrictMode>
);

View File

@ -1,532 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Typography,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Tag,
Card,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
CopyOutlined,
} from '@ant-design/icons';
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
const { Option } = Select;
const Applications: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editingApp, setEditingApp] = useState<Application | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedApp, setSelectedApp] = useState<Application | null>(null);
const [form] = Form.useForm();
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications();
setApplications(response.data);
} catch (error) {
console.error('Failed to load applications:', error);
message.error('Failed to load applications');
} finally {
setLoading(false);
}
};
const handleCreate = () => {
setEditingApp(null);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (app: Application) => {
setEditingApp(app);
form.setFieldsValue({
app_id: app.app_id,
app_link: app.app_link,
type: app.type,
callback_url: app.callback_url,
token_prefix: app.token_prefix,
token_renewal_duration: formatDuration(app.token_renewal_duration),
max_token_duration: formatDuration(app.max_token_duration),
owner_type: app.owner.type,
owner_name: app.owner.name,
owner_owner: app.owner.owner,
});
setModalVisible(true);
};
const handleDelete = async (appId: string) => {
try {
await apiService.deleteApplication(appId);
message.success('Application deleted successfully');
loadApplications();
} catch (error) {
console.error('Failed to delete application:', error);
message.error('Failed to delete application');
}
};
const handleSubmit = async (values: any) => {
try {
const requestData: CreateApplicationRequest = {
app_id: values.app_id,
app_link: values.app_link,
type: values.type,
callback_url: values.callback_url,
token_prefix: values.token_prefix,
token_renewal_duration: values.token_renewal_duration,
max_token_duration: values.max_token_duration,
owner: {
type: values.owner_type,
name: values.owner_name,
owner: values.owner_owner,
},
};
if (editingApp) {
await apiService.updateApplication(editingApp.app_id, requestData);
message.success('Application updated successfully');
} else {
await apiService.createApplication(requestData);
message.success('Application created successfully');
}
setModalVisible(false);
loadApplications();
} catch (error) {
console.error('Failed to save application:', error);
message.error('Failed to save application');
}
};
const showDetails = (app: Application) => {
setSelectedApp(app);
setDetailModalVisible(true);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('Copied to clipboard');
};
const formatDuration = (nanoseconds: number): string => {
const hours = Math.floor(nanoseconds / (1000000000 * 60 * 60));
return `${hours}h`;
};
const columns = [
{
title: 'App ID',
dataIndex: 'app_id',
key: 'app_id',
render: (text: string) => <Text code>{text}</Text>,
},
{
title: 'App Link',
dataIndex: 'app_link',
key: 'app_link',
render: (text: string) => (
<a href={text} target="_blank" rel="noopener noreferrer">
{text}
</a>
),
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (types: string[]) => (
<>
{types.map((type) => (
<Tag key={type} color={type === 'static' ? 'blue' : 'green'}>
{type.toUpperCase()}
</Tag>
))}
</>
),
},
{
title: 'Token Prefix',
dataIndex: 'token_prefix',
key: 'token_prefix',
render: (prefix: string) => prefix ? <Text code>{prefix}</Text> : <Text type="secondary">Default</Text>,
},
{
title: 'Owner',
dataIndex: 'owner',
key: 'owner',
render: (owner: Application['owner']) => (
<div>
<div>{owner.name}</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{owner.type} {owner.owner}
</Text>
</div>
),
},
{
title: 'Created',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => dayjs(date).format('MMM DD, YYYY'),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: Application) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showDetails(record)}
title="View Details"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
title="Edit"
/>
<Popconfirm
title="Are you sure you want to delete this application?"
onConfirm={() => handleDelete(record.app_id)}
okText="Yes"
cancelText="No"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
title="Delete"
/>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Applications</Title>
<Text type="secondary">
Manage your applications and their configurations
</Text>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
Create Application
</Button>
</div>
<Table
columns={columns}
dataSource={applications}
rowKey="app_id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} applications`,
}}
/>
</Space>
{/* Create/Edit Modal */}
<Modal
title={editingApp ? 'Edit Application' : 'Create Application'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={() => form.submit()}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" disabled={!!editingApp} />
</Form.Item>
<Form.Item
name="app_link"
label="Application Link"
rules={[
{ required: true, message: 'Please enter application link' },
{ type: 'url', message: 'Please enter a valid URL' },
]}
>
<Input placeholder="https://example.com" />
</Form.Item>
<Form.Item
name="type"
label="Application Type"
rules={[{ required: true, message: 'Please select application type' }]}
>
<Select mode="multiple" placeholder="Select types">
<Option value="static">Static</Option>
<Option value="user">User</Option>
</Select>
</Form.Item>
<Form.Item
name="callback_url"
label="Callback URL"
rules={[
{ required: true, message: 'Please enter callback URL' },
{ type: 'url', message: 'Please enter a valid URL' },
]}
>
<Input placeholder="https://example.com/callback" />
</Form.Item>
<Form.Item
name="token_prefix"
label="Token Prefix"
rules={[
{
pattern: /^[A-Z]{2,4}$/,
message: 'Token prefix must be 2-4 uppercase letters (e.g., NC for Nerd Completion)'
},
]}
help="Optional custom prefix for tokens. Leave empty for default 'kms_' prefix. Examples: NC → NCT- (static), NCUT- (user)"
>
<Input
placeholder="NC"
maxLength={4}
style={{ textTransform: 'uppercase' }}
onChange={(e) => {
const value = e.target.value.toUpperCase();
form.setFieldValue('token_prefix', value);
}}
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="token_renewal_duration"
label="Token Renewal Duration"
rules={[{ required: true, message: 'Please enter duration' }]}
>
<Input placeholder="168h" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="max_token_duration"
label="Max Token Duration"
rules={[{ required: true, message: 'Please enter duration' }]}
>
<Input placeholder="720h" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="owner_type"
label="Owner Type"
rules={[{ required: true, message: 'Please select owner type' }]}
>
<Select placeholder="Select owner type">
<Option value="individual">Individual</Option>
<Option value="team">Team</Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="owner_name"
label="Owner Name"
rules={[{ required: true, message: 'Please enter owner name' }]}
>
<Input placeholder="John Doe" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="owner_owner"
label="Owner Contact"
rules={[{ required: true, message: 'Please enter owner contact' }]}
>
<Input placeholder="john.doe@example.com" />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
{/* Details Modal */}
<Modal
title="Application Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
Close
</Button>,
]}
width={700}
>
{selectedApp && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Basic Information">
<Row gutter={16}>
<Col span={12}>
<Text strong>App ID:</Text>
<div>
<Text code>{selectedApp.app_id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedApp.app_id)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>App Link:</Text>
<div>
<a href={selectedApp.app_link} target="_blank" rel="noopener noreferrer">
{selectedApp.app_link}
</a>
</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Type:</Text>
<div>
{selectedApp.type.map((type) => (
<Tag key={type} color={type === 'static' ? 'blue' : 'green'}>
{type.toUpperCase()}
</Tag>
))}
</div>
</Col>
<Col span={12}>
<Text strong>Callback URL:</Text>
<div>{selectedApp.callback_url}</div>
</Col>
</Row>
</Card>
<Card title="Security Configuration">
<Row gutter={16}>
<Col span={12}>
<Text strong>HMAC Key:</Text>
<div>
<Text code></Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedApp.hmac_key)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>Token Prefix:</Text>
<div>
{selectedApp.token_prefix ? (
<>
<Text code>{selectedApp.token_prefix}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>
(Static: {selectedApp.token_prefix}T-, User: {selectedApp.token_prefix}UT-)
</Text>
</>
) : (
<Text type="secondary">Default (kms_)</Text>
)}
</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Token Renewal Duration:</Text>
<div>{formatDuration(selectedApp.token_renewal_duration)}</div>
</Col>
<Col span={12}>
<Text strong>Max Token Duration:</Text>
<div>{formatDuration(selectedApp.max_token_duration)}</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedApp.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedApp.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedApp.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedApp.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedApp.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedApp.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Applications;

View File

@ -1,465 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Typography,
Space,
Tag,
DatePicker,
Select,
Input,
Button,
Row,
Col,
Alert,
Timeline,
} from 'antd';
import {
AuditOutlined,
SearchOutlined,
FilterOutlined,
UserOutlined,
AppstoreOutlined,
KeyOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { apiService, AuditEvent, AuditQueryParams } from '../services/apiService';
dayjs.extend(relativeTime);
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const { Option } = Select;
const Audit: React.FC = () => {
const [auditData, setAuditData] = useState<AuditEvent[]>([]);
const [filteredData, setFilteredData] = useState<AuditEvent[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
dateRange: null as any,
action: '',
status: '',
user: '',
resourceType: '',
});
// Load audit data on component mount
useEffect(() => {
loadAuditData();
}, []);
const loadAuditData = async () => {
try {
setLoading(true);
const response = await apiService.getAuditEvents({
limit: 100,
order_by: 'timestamp',
order_desc: true,
});
setAuditData(response.events);
setFilteredData(response.events);
} catch (error) {
console.error('Failed to load audit data:', error);
// Keep empty arrays on error
setAuditData([]);
setFilteredData([]);
} finally {
setLoading(false);
}
};
const applyFilters = async () => {
// For real-time filtering, we'll use the API with filters
try {
setLoading(true);
const params: AuditQueryParams = {
limit: 100,
order_by: 'timestamp',
order_desc: true,
};
if (filters.dateRange && filters.dateRange.length === 2) {
const [start, end] = filters.dateRange;
params.start_time = start.toISOString();
params.end_time = end.toISOString();
}
if (filters.action) {
params.event_types = [filters.action];
}
if (filters.status) {
params.statuses = [filters.status];
}
if (filters.user) {
params.actor_id = filters.user;
}
if (filters.resourceType) {
params.resource_type = filters.resourceType;
}
const response = await apiService.getAuditEvents(params);
setFilteredData(response.events);
} catch (error) {
console.error('Failed to apply filters:', error);
} finally {
setLoading(false);
}
};
const clearFilters = () => {
setFilters({
dateRange: null,
action: '',
status: '',
user: '',
resourceType: '',
});
loadAuditData(); // Reload original data
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'success':
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
case 'failure':
return <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />;
case 'warning':
return <ExclamationCircleOutlined style={{ color: '#faad14' }} />;
default:
return <ClockCircleOutlined style={{ color: '#1890ff' }} />;
}
};
const getActionIcon = (action: string) => {
if (action.includes('APPLICATION')) return <AppstoreOutlined />;
if (action.includes('TOKEN')) return <KeyOutlined />;
if (action.includes('USER')) return <UserOutlined />;
return <AuditOutlined />;
};
const columns = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
render: (timestamp: string) => (
<div>
<div>{dayjs(timestamp).format('MMM DD, YYYY')}</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{dayjs(timestamp).format('HH:mm:ss')}
</Text>
</div>
),
sorter: (a: AuditEvent, b: AuditEvent) =>
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
defaultSortOrder: 'descend' as const,
},
{
title: 'User',
dataIndex: 'actor_id',
key: 'actor_id',
render: (actorId: string) => (
<div>
<UserOutlined style={{ marginRight: '8px' }} />
{actorId || 'System'}
</div>
),
},
{
title: 'Action',
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<div>
{getActionIcon(type)}
<span style={{ marginLeft: '8px' }}>{type.replace(/_/g, ' ').replace(/\./g, ' ')}</span>
</div>
),
},
{
title: 'Resource',
key: 'resource',
render: (_: any, record: AuditEvent) => (
<div>
<div>
<Tag color="blue">{record.resource_type?.toUpperCase() || 'N/A'}</Tag>
</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{record.resource_id || 'N/A'}
</Text>
</div>
),
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag
color={status === 'success' ? 'green' : status === 'failure' ? 'red' : 'orange'}
icon={getStatusIcon(status)}
>
{status.toUpperCase()}
</Tag>
),
},
{
title: 'IP Address',
dataIndex: 'actor_ip',
key: 'actor_ip',
render: (ip: string) => <Text code>{ip || 'N/A'}</Text>,
},
];
const expandedRowRender = (record: AuditEvent) => (
<Card size="small" title="Event Details">
<Row gutter={16}>
<Col span={12}>
<Space direction="vertical" size="small">
<div>
<Text strong>User Agent:</Text>
<div style={{ wordBreak: 'break-all' }}>
<Text type="secondary">{record.user_agent}</Text>
</div>
</div>
<div>
<Text strong>Event ID:</Text>
<div><Text code>{record.id}</Text></div>
</div>
</Space>
</Col>
<Col span={12}>
<div>
<Text strong>Additional Details:</Text>
<pre style={{
background: '#f5f5f5',
padding: '8px',
borderRadius: '4px',
fontSize: '12px',
marginTop: '8px'
}}>
{JSON.stringify(record.details, null, 2)}
</pre>
</div>
</Col>
</Row>
</Card>
);
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={2}>Audit Log</Title>
<Text type="secondary">
Monitor and track all system activities and security events
</Text>
</div>
{/* Statistics Cards */}
<Row gutter={16}>
<Col span={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<AuditOutlined style={{ fontSize: '32px', color: '#1890ff', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>{filteredData.length}</div>
<div>Total Events</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<CheckCircleOutlined style={{ fontSize: '32px', color: '#52c41a', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
{filteredData.filter(e => e.status === 'success').length}
</div>
<div>Successful</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<ExclamationCircleOutlined style={{ fontSize: '32px', color: '#ff4d4f', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
{filteredData.filter(e => e.status === 'failure').length}
</div>
<div>Failed</div>
</div>
</Card>
</Col>
<Col span={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<UserOutlined style={{ fontSize: '32px', color: '#722ed1', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
{new Set(filteredData.map(e => e.actor_id).filter(id => id)).size}
</div>
<div>Unique Users</div>
</div>
</Card>
</Col>
</Row>
{/* Filters */}
<Card title="Filters" extra={
<Space>
<Button onClick={applyFilters} type="primary" icon={<SearchOutlined />}>
Apply Filters
</Button>
<Button onClick={clearFilters} icon={<DeleteOutlined />}>
Clear
</Button>
</Space>
}>
<Row gutter={16}>
<Col span={6}>
<div style={{ marginBottom: '8px' }}>
<Text strong>Date Range:</Text>
</div>
<RangePicker
style={{ width: '100%' }}
value={filters.dateRange}
onChange={(dates) => setFilters({ ...filters, dateRange: dates })}
/>
</Col>
<Col span={4}>
<div style={{ marginBottom: '8px' }}>
<Text strong>Action:</Text>
</div>
<Select
style={{ width: '100%' }}
placeholder="All actions"
value={filters.action}
onChange={(value) => setFilters({ ...filters, action: value })}
allowClear
>
<Option value="app.created">Application Created</Option>
<Option value="app.updated">Application Updated</Option>
<Option value="app.deleted">Application Deleted</Option>
<Option value="auth.token_created">Token Created</Option>
<Option value="auth.token_revoked">Token Revoked</Option>
<Option value="auth.token_validated">Token Validated</Option>
<Option value="auth.login">Login</Option>
<Option value="auth.login_failed">Login Failed</Option>
</Select>
</Col>
<Col span={4}>
<div style={{ marginBottom: '8px' }}>
<Text strong>Status:</Text>
</div>
<Select
style={{ width: '100%' }}
placeholder="All statuses"
value={filters.status}
onChange={(value) => setFilters({ ...filters, status: value })}
allowClear
>
<Option value="success">Success</Option>
<Option value="failure">Failure</Option>
<Option value="warning">Warning</Option>
</Select>
</Col>
<Col span={5}>
<div style={{ marginBottom: '8px' }}>
<Text strong>User:</Text>
</div>
<Input
placeholder="Search by user"
value={filters.user}
onChange={(e) => setFilters({ ...filters, user: e.target.value })}
allowClear
/>
</Col>
<Col span={5}>
<div style={{ marginBottom: '8px' }}>
<Text strong>Resource Type:</Text>
</div>
<Select
style={{ width: '100%' }}
placeholder="All types"
value={filters.resourceType}
onChange={(value) => setFilters({ ...filters, resourceType: value })}
allowClear
>
<Option value="application">Application</Option>
<Option value="token">Token</Option>
<Option value="user">User</Option>
</Select>
</Col>
</Row>
</Card>
{/* Recent Activity Timeline */}
<Card title="Recent Activity">
<Timeline>
{filteredData.slice(0, 5).map((entry) => (
<Timeline.Item
key={entry.id}
dot={getStatusIcon(entry.status)}
color={entry.status === 'success' ? 'green' : entry.status === 'failure' ? 'red' : 'orange'}
>
<div>
<Text strong>{entry.type.replace(/_/g, ' ').replace(/\./g, ' ')}</Text>
<div>
<Text type="secondary">
{entry.actor_id || 'System'} {dayjs(entry.timestamp).fromNow()}
</Text>
</div>
<div>
<Tag>{entry.resource_type || 'N/A'}</Tag>
<Text type="secondary" style={{ marginLeft: '8px' }}>
{entry.resource_id || 'N/A'}
</Text>
</div>
</div>
</Timeline.Item>
))}
</Timeline>
</Card>
{/* Audit Log Table */}
<Card title="Audit Log Entries">
{filteredData.length === 0 && !loading && (
<Alert
message="No Audit Events Found"
description="No audit events match your current filters. Try adjusting the filters or check if any events have been logged to the system."
type="info"
showIcon
style={{ marginBottom: '16px' }}
/>
)}
<Table
columns={columns}
dataSource={filteredData}
rowKey="id"
loading={loading}
expandable={{
expandedRowRender,
expandRowByClick: true,
}}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} audit entries`,
}}
/>
</Card>
</Space>
</div>
);
};
export default Audit;

View File

@ -1,228 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Statistic, Typography, Space, Alert, Spin } from 'antd';
import {
AppstoreOutlined,
KeyOutlined,
UserOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { apiService } from '../services/apiService';
const { Title } = Typography;
interface DashboardStats {
totalApplications: number;
totalTokens: number;
healthStatus: 'healthy' | 'unhealthy';
readinessStatus: 'ready' | 'not-ready';
}
const Dashboard: React.FC = () => {
const [stats, setStats] = useState<DashboardStats>({
totalApplications: 0,
totalTokens: 0,
healthStatus: 'unhealthy',
readinessStatus: 'not-ready',
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
setError('');
// Load health status
const [healthResponse, readinessResponse] = await Promise.allSettled([
apiService.healthCheck(),
apiService.readinessCheck(),
]);
const healthStatus = healthResponse.status === 'fulfilled' ? 'healthy' : 'unhealthy';
const readinessStatus = readinessResponse.status === 'fulfilled' ? 'ready' : 'not-ready';
// Load applications count
let totalApplications = 0;
let totalTokens = 0;
try {
const appsResponse = await apiService.getApplications(100, 0);
totalApplications = appsResponse.count;
// Count tokens across all applications
for (const app of appsResponse.data) {
try {
const tokensResponse = await apiService.getTokensForApplication(app.app_id, 100, 0);
totalTokens += tokensResponse.count;
} catch (tokenError) {
console.warn(`Failed to load tokens for app ${app.app_id}:`, tokenError);
}
}
} catch (appsError) {
console.warn('Failed to load applications:', appsError);
}
setStats({
totalApplications,
totalTokens,
healthStatus,
readinessStatus,
});
} catch (err) {
console.error('Dashboard error:', err);
setError('Failed to load dashboard data. Please check your connection.');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>Loading dashboard...</div>
</div>
);
}
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={2}>Dashboard</Title>
<p>Welcome to the Key Management System dashboard. Monitor your applications, tokens, and system health.</p>
</div>
{error && (
<Alert
message="Error"
description={error}
type="error"
showIcon
closable
onClose={() => setError('')}
/>
)}
{/* System Status */}
<Card title="System Status" style={{ marginBottom: '24px' }}>
<Row gutter={16}>
<Col span={12}>
<Card>
<Statistic
title="Health Status"
value={stats.healthStatus === 'healthy' ? 'Healthy' : 'Unhealthy'}
prefix={
stats.healthStatus === 'healthy' ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)
}
valueStyle={{
color: stats.healthStatus === 'healthy' ? '#52c41a' : '#ff4d4f',
}}
/>
</Card>
</Col>
<Col span={12}>
<Card>
<Statistic
title="Readiness Status"
value={stats.readinessStatus === 'ready' ? 'Ready' : 'Not Ready'}
prefix={
stats.readinessStatus === 'ready' ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)
}
valueStyle={{
color: stats.readinessStatus === 'ready' ? '#52c41a' : '#ff4d4f',
}}
/>
</Card>
</Col>
</Row>
</Card>
{/* Statistics */}
<Row gutter={16}>
<Col xs={24} sm={12} lg={8}>
<Card>
<Statistic
title="Total Applications"
value={stats.totalApplications}
prefix={<AppstoreOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={8}>
<Card>
<Statistic
title="Total Tokens"
value={stats.totalTokens}
prefix={<KeyOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={8}>
<Card>
<Statistic
title="Active Users"
value={1}
prefix={<UserOutlined />}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
</Row>
{/* Quick Actions */}
<Card title="Quick Actions">
<Row gutter={16}>
<Col xs={24} sm={8}>
<Card
hoverable
onClick={() => window.location.pathname = '/applications'}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<AppstoreOutlined style={{ fontSize: '32px', color: '#1890ff', marginBottom: '8px' }} />
<div>Manage Applications</div>
</Card>
</Col>
<Col xs={24} sm={8}>
<Card
hoverable
onClick={() => window.location.pathname = '/tokens'}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<KeyOutlined style={{ fontSize: '32px', color: '#52c41a', marginBottom: '8px' }} />
<div>Manage Tokens</div>
</Card>
</Col>
<Col xs={24} sm={8}>
<Card
hoverable
onClick={() => window.location.pathname = '/audit'}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<ExclamationCircleOutlined style={{ fontSize: '32px', color: '#fa8c16', marginBottom: '8px' }} />
<div>View Audit Log</div>
</Card>
</Col>
</Row>
</Card>
</Space>
</div>
);
};
export default Dashboard;

View File

@ -1,112 +0,0 @@
import React, { useState } from 'react';
import { Form, Input, Button, Card, Typography, Space, Alert } from 'antd';
import { UserOutlined, KeyOutlined } from '@ant-design/icons';
import { useAuth } from '../contexts/AuthContext';
const { Title, Text } = Typography;
const Login: React.FC = () => {
const [form] = Form.useForm();
const { login, loading } = useAuth();
const [error, setError] = useState<string>('');
const onFinish = async (values: { email: string }) => {
setError('');
const success = await login(values.email);
if (!success) {
setError('Login failed. Please check your email and try again.');
}
};
return (
<div style={{
minHeight: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '20px'
}}>
<Card
style={{
width: '100%',
maxWidth: 400,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
borderRadius: '12px'
}}
>
<Space direction="vertical" size="large" style={{ width: '100%', textAlign: 'center' }}>
<div>
<KeyOutlined style={{ fontSize: '48px', color: '#1890ff', marginBottom: '16px' }} />
<Title level={2} style={{ margin: 0, color: '#262626' }}>
KMS Login
</Title>
<Text type="secondary">
Key Management System
</Text>
</div>
{error && (
<Alert
message={error}
type="error"
showIcon
closable
onClose={() => setError('')}
/>
)}
<Alert
message="Demo Login"
description="Enter any email address to access the demo. In production, this would integrate with your authentication system."
type="info"
showIcon
/>
<Form
form={form}
name="login"
onFinish={onFinish}
layout="vertical"
size="large"
>
<Form.Item
name="email"
label="Email Address"
rules={[
{ required: true, message: 'Please input your email!' },
{ type: 'email', message: 'Please enter a valid email address!' }
]}
>
<Input
prefix={<UserOutlined />}
placeholder="Enter your email"
autoComplete="email"
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
block
style={{ height: '44px', fontSize: '16px' }}
>
{loading ? 'Signing In...' : 'Sign In'}
</Button>
</Form.Item>
</Form>
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
Demo credentials: Use any valid email address
</Text>
</div>
</Space>
</Card>
</div>
);
};
export default Login;

View File

@ -1,719 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Button,
Form,
Input,
Select,
Space,
Typography,
Alert,
Divider,
Row,
Col,
Tag,
Checkbox,
message,
Modal,
Steps,
} from 'antd';
import {
PlayCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
CopyOutlined,
ReloadOutlined,
LinkOutlined,
} from '@ant-design/icons';
import { apiService, Application } from '../services/apiService';
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
const { TextArea } = Input;
const { Step } = Steps;
interface LoginTestResult {
success: boolean;
token?: string;
redirectUrl?: string;
userId?: string;
appId?: string;
expiresIn?: number;
error?: string;
timestamp: string;
}
interface CallbackTestResult {
success: boolean;
token?: string;
verified?: boolean;
permitted?: boolean;
user_id?: string;
permissions?: string[];
permission_results?: Record<string, boolean>;
expires_at?: string;
max_valid_at?: string;
token_type?: string;
error?: string;
timestamp: string;
}
const availablePermissions = [
'app.read',
'app.write',
'app.delete',
'token.read',
'token.create',
'token.revoke',
'repo.read',
'repo.write',
'repo.admin',
'permission.read',
'permission.write',
'permission.grant',
'permission.revoke',
];
const TokenTester: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [testLoading, setTestLoading] = useState(false);
const [callbackLoading, setCallbackLoading] = useState(false);
const [loginResult, setLoginResult] = useState<LoginTestResult | null>(null);
const [callbackResult, setCallbackResult] = useState<CallbackTestResult | null>(null);
const [currentStep, setCurrentStep] = useState(0);
const [callbackModalVisible, setCallbackModalVisible] = useState(false);
const [form] = Form.useForm();
const [callbackForm] = Form.useForm();
const [useCallback, setUseCallback] = useState(false);
const [extractedToken, setExtractedToken] = useState('');
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications();
setApplications(response.data);
} catch (error) {
console.error('Failed to load applications:', error);
message.error('Failed to load applications');
} finally {
setLoading(false);
}
};
const handleLoginTest = async (values: any) => {
try {
setTestLoading(true);
setCurrentStep(1);
const selectedApp = applications.find(app => app.app_id === values.app_id);
const callbackUrl = `${window.location.origin}/token-tester/callback`;
// Store test data in localStorage for the callback page
const testData = {
app_id: values.app_id,
permissions: values.permissions || [],
use_callback: values.use_callback,
timestamp: new Date().toISOString(),
};
localStorage.setItem('token_tester_data', JSON.stringify(testData));
console.log('Testing login flow with:', {
app_id: values.app_id,
permissions: values.permissions || [],
redirect_uri: values.use_callback ? callbackUrl : undefined,
});
const response = await apiService.login(
values.app_id,
values.permissions || [],
values.use_callback ? callbackUrl : undefined,
values.token_delivery || 'query'
);
console.log('Login response:', response);
const result: LoginTestResult = {
success: true,
token: response.token,
redirectUrl: response.redirect_url,
userId: response.user_id,
appId: values.app_id,
expiresIn: response.expires_in,
timestamp: new Date().toISOString(),
};
setLoginResult(result);
setCurrentStep(2);
message.success('Login test completed successfully!');
// If we have a redirect URL, show the callback modal
if (response.redirect_url && values.use_callback) {
setCallbackModalVisible(true);
// Extract token from redirect URL if using query parameter delivery
let tokenFromUrl = '';
if (values.token_delivery === 'query') {
try {
const url = new URL(response.redirect_url);
tokenFromUrl = url.searchParams.get('token') || '';
} catch (e) {
console.warn('Failed to parse redirect URL for token extraction:', e);
}
}
setExtractedToken(tokenFromUrl);
callbackForm.setFieldsValue({
app_id: values.app_id,
token: tokenFromUrl, // Pre-fill with extracted token if available
permissions: values.permissions || [],
});
}
} catch (error: any) {
console.error('Login test failed:', error);
const result: LoginTestResult = {
success: false,
error: error.response?.data?.message || error.message || 'Login test failed',
timestamp: new Date().toISOString(),
};
setLoginResult(result);
setCurrentStep(2);
message.error('Login test failed');
} finally {
setTestLoading(false);
}
};
const handleCallbackTest = async (values: any) => {
try {
setCallbackLoading(true);
setCurrentStep(3);
console.log('Testing callback with token verification:', values);
// Verify the token received in the callback (type will be auto-detected)
const verifyResponse = await apiService.verifyToken({
app_id: values.app_id,
token: values.token,
permissions: values.permissions || [],
});
console.log('Token verification response:', verifyResponse);
const result: CallbackTestResult = {
success: verifyResponse.valid,
token: values.token,
verified: verifyResponse.valid,
permitted: verifyResponse.permitted,
user_id: verifyResponse.user_id,
permissions: verifyResponse.permissions,
permission_results: verifyResponse.permission_results,
expires_at: verifyResponse.expires_at,
max_valid_at: verifyResponse.max_valid_at,
token_type: verifyResponse.token_type,
error: verifyResponse.error,
timestamp: new Date().toISOString(),
};
setCallbackResult(result);
setCurrentStep(4);
if (verifyResponse.valid) {
message.success('Callback test completed successfully!');
// Auto-close modal after successful verification to show results
setTimeout(() => {
setCallbackModalVisible(false);
}, 1500);
} else {
message.warning('Callback test completed - token verification failed');
// Auto-close modal after failed verification to show results
setTimeout(() => {
setCallbackModalVisible(false);
}, 1500);
}
} catch (error: any) {
console.error('Callback test failed:', error);
const result: CallbackTestResult = {
success: false,
error: error.response?.data?.message || error.message || 'Callback test failed',
timestamp: new Date().toISOString(),
};
setCallbackResult(result);
setCurrentStep(4);
message.error('Callback test failed');
// Auto-close modal to show error results
setTimeout(() => {
setCallbackModalVisible(false);
}, 1500);
} finally {
setCallbackLoading(false);
}
};
const resetTest = () => {
setCurrentStep(0);
setLoginResult(null);
setCallbackResult(null);
setCallbackModalVisible(false);
setUseCallback(false);
setExtractedToken('');
form.resetFields();
callbackForm.resetFields();
// Clear stored test data
localStorage.removeItem('token_tester_data');
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('Copied to clipboard');
};
const openCallbackUrl = () => {
if (loginResult?.redirectUrl) {
window.open(loginResult.redirectUrl, '_blank');
}
};
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Token Tester</Title>
<Text type="secondary">
Test the /login flow and callback handling for user tokens
</Text>
<div style={{ marginTop: '8px' }}>
<Alert
message="Two Testing Modes Available"
description={
<div>
<Text strong>Direct Mode:</Text> Login returns token directly in response body (no callback)<br/>
<Text strong>Callback Mode:</Text> Login returns redirect URL, token in query parameter (default) or secure cookie
</div>
}
type="info"
showIcon
style={{ fontSize: '12px' }}
/>
</div>
</div>
<Button
icon={<ReloadOutlined />}
onClick={resetTest}
disabled={testLoading || callbackLoading}
>
Reset Test
</Button>
</div>
{/* Test Progress */}
<Card title="Test Progress">
<Steps current={currentStep} size="small">
<Step title="Configure" description="Set up test parameters" />
<Step title="Login Test" description="Test /login endpoint" />
<Step title="Results" description="Review login results" />
<Step title="Callback Test" description="Test callback handling" />
<Step title="Complete" description="Test completed" />
</Steps>
</Card>
{/* Test Configuration */}
<Card title="Test Configuration">
<Form
form={form}
layout="vertical"
onFinish={handleLoginTest}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="app_id"
label="Application"
rules={[{ required: true, message: 'Please select an application' }]}
>
<Select
placeholder="Select application to test"
loading={loading}
>
{applications.map(app => (
<Option key={app.app_id} value={app.app_id}>
<div>
<Text strong>{app.app_id}</Text>
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
{app.app_link}
</Text>
</div>
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="use_callback"
valuePropName="checked"
label=" "
>
<Checkbox onChange={(e) => setUseCallback(e.target.checked)}>
Use callback URL (test full flow)
</Checkbox>
</Form.Item>
</Col>
</Row>
<Form.Item
name="token_delivery"
label="Token Delivery Method (for callback flows)"
tooltip="Choose how tokens are delivered when using callback URLs"
initialValue="query"
>
<Select placeholder="Select delivery method" disabled={!useCallback} defaultValue="query">
<Option value="query">
<div>
<Text strong>Query Parameter</Text> (Recommended for testing)
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
Token included in callback URL query string
</Text>
</div>
</Option>
<Option value="cookie">
<div>
<Text strong>Cookie</Text> (More secure for production)
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
Token stored in HTTP-only cookie
</Text>
</div>
</Option>
</Select>
</Form.Item>
<Row gutter={16}>
</Row>
<Form.Item
name="permissions"
label="Permissions to Request"
>
<Checkbox.Group>
<Row>
{availablePermissions.map(permission => (
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
<Checkbox value={permission}>{permission}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
icon={<PlayCircleOutlined />}
loading={testLoading}
size="large"
>
Start Login Test
</Button>
</Form.Item>
</Form>
</Card>
{/* Login Test Results */}
{loginResult && (
<Card
title={
<Space>
{loginResult.success ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)}
Login Test Results
</Space>
}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
message={loginResult.success ? 'Login Test Successful' : 'Login Test Failed'}
description={loginResult.success
? 'The /login endpoint responded successfully'
: loginResult.error
}
type={loginResult.success ? 'success' : 'error'}
showIcon
/>
{loginResult.success && (
<div>
<Row gutter={16}>
{loginResult.token && (
<Col span={12}>
<Card size="small" title="User Token">
<Space direction="vertical" style={{ width: '100%' }}>
<TextArea
value={loginResult.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace', fontSize: '12px' }}
/>
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(loginResult.token!)}
>
Copy Token
</Button>
</Space>
</Card>
</Col>
)}
{loginResult.redirectUrl && (
<Col span={12}>
<Card size="small" title="Redirect URL">
<Space direction="vertical" style={{ width: '100%' }}>
<Text code style={{ fontSize: '12px', wordBreak: 'break-all' }}>
{loginResult.redirectUrl}
</Text>
<Space>
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(loginResult.redirectUrl!)}
>
Copy URL
</Button>
<Button
size="small"
icon={<LinkOutlined />}
onClick={openCallbackUrl}
>
Open URL
</Button>
</Space>
</Space>
</Card>
</Col>
)}
</Row>
<Divider />
<Row gutter={16}>
<Col span={6}>
<Text strong>User ID:</Text>
<div>{loginResult.userId || 'N/A'}</div>
</Col>
<Col span={6}>
<Text strong>App ID:</Text>
<div>{loginResult.appId}</div>
</Col>
<Col span={6}>
<Text strong>Expires In:</Text>
<div>{loginResult.expiresIn ? `${loginResult.expiresIn}s` : 'N/A'}</div>
</Col>
<Col span={6}>
<Text strong>Timestamp:</Text>
<div>{new Date(loginResult.timestamp).toLocaleString()}</div>
</Col>
</Row>
</div>
)}
</Space>
</Card>
)}
{/* Callback Test Results */}
{callbackResult && (
<Card
title={
<Space>
{callbackResult.success ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)}
Callback Test Results
</Space>
}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
message={callbackResult.success ? 'Callback Test Successful' : 'Callback Test Failed'}
description={callbackResult.success
? 'Token verification in callback was successful'
: callbackResult.error
}
type={callbackResult.success ? 'success' : 'error'}
showIcon
/>
{callbackResult.success && (
<div>
<Row gutter={16}>
<Col span={12}>
<Card size="small" title="Token Information">
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Text strong>Token Type:</Text>
<div>
<Tag color="blue">{(callbackResult.token_type || 'user').toUpperCase()}</Tag>
</div>
</div>
{callbackResult.user_id && (
<div>
<Text strong>User ID:</Text>
<div>{callbackResult.user_id}</div>
</div>
)}
{callbackResult.expires_at && (
<div>
<Text strong>Expires At:</Text>
<div>{new Date(callbackResult.expires_at).toLocaleString()}</div>
</div>
)}
{callbackResult.max_valid_at && (
<div>
<Text strong>Max Valid Until:</Text>
<div>{new Date(callbackResult.max_valid_at).toLocaleString()}</div>
</div>
)}
</Space>
</Card>
</Col>
<Col span={12}>
<Card size="small" title="Permissions">
<Space direction="vertical" style={{ width: '100%' }}>
{callbackResult.permissions && callbackResult.permissions.length > 0 ? (
<div>
<Text strong>Available Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{callbackResult.permissions.map(permission => (
<Tag key={permission} color="green" style={{ margin: '2px' }}>
{permission}
</Tag>
))}
</div>
</div>
) : (
<Text type="secondary">No permissions available</Text>
)}
{callbackResult.permission_results && Object.keys(callbackResult.permission_results).length > 0 && (
<div style={{ marginTop: '16px' }}>
<Text strong>Permission Check Results:</Text>
<div style={{ marginTop: '8px' }}>
{Object.entries(callbackResult.permission_results).map(([permission, granted]) => (
<div key={permission} style={{ marginBottom: '4px' }}>
<Tag color={granted ? 'green' : 'red'}>
{permission}: {granted ? 'GRANTED' : 'DENIED'}
</Tag>
</div>
))}
</div>
</div>
)}
</Space>
</Card>
</Col>
</Row>
</div>
)}
<div style={{ marginTop: '16px' }}>
<Text strong>Timestamp:</Text>
<div>{new Date(callbackResult.timestamp).toLocaleString()}</div>
</div>
</Space>
</Card>
)}
</Space>
{/* Callback Test Modal */}
<Modal
title="Test Callback Handling"
open={callbackModalVisible}
onCancel={() => setCallbackModalVisible(false)}
onOk={() => callbackForm.submit()}
confirmLoading={callbackLoading}
width={700}
>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Callback URL Received"
description={extractedToken
? "Token successfully extracted from callback URL. Verify the token to complete the flow test."
: "Redirect URL received. If using cookie delivery, the token is stored in a secure cookie."
}
type="info"
showIcon
/>
<Form
form={callbackForm}
layout="vertical"
onFinish={handleCallbackTest}
>
<Form.Item
name="app_id"
label="Application ID"
>
<Input readOnly />
</Form.Item>
<Form.Item
name="token"
label="Token from Callback"
rules={[{ required: true, message: 'Token is required' }]}
>
<TextArea
rows={3}
style={{ fontFamily: 'monospace' }}
placeholder="Token extracted from callback URL"
/>
</Form.Item>
<Form.Item
name="permissions"
label="Permissions to Verify"
>
<Checkbox.Group disabled>
<Row>
{availablePermissions.map(permission => (
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
<Checkbox value={permission}>{permission}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
</Form>
</Space>
</Modal>
</div>
);
};
export default TokenTester;

View File

@ -1,396 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Card,
Button,
Space,
Typography,
Alert,
Row,
Col,
Tag,
Spin,
Result,
Input,
} from 'antd';
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
CopyOutlined,
ArrowLeftOutlined,
} from '@ant-design/icons';
import { apiService } from '../services/apiService';
const { Title, Text } = Typography;
const { TextArea } = Input;
interface CallbackData {
token?: string;
state?: string;
error?: string;
error_description?: string;
}
interface VerificationResult {
valid: boolean;
permitted: boolean;
user_id?: string;
permissions: string[];
permission_results?: Record<string, boolean>;
expires_at?: string;
max_valid_at?: string;
token_type: string;
claims?: Record<string, string>;
error?: string;
}
const TokenTesterCallback: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [callbackData, setCallbackData] = useState<CallbackData>({});
const [verificationResult, setVerificationResult] = useState<VerificationResult | null>(null);
const [verificationError, setVerificationError] = useState<string | null>(null);
useEffect(() => {
parseCallbackData();
}, [location]);
const parseCallbackData = async () => {
try {
setLoading(true);
// Parse URL parameters
const urlParams = new URLSearchParams(location.search);
let token = urlParams.get('token') || undefined;
// If no token in URL, try to extract from auth_token cookie
if (!token) {
token = getCookie('auth_token') || undefined;
}
const data: CallbackData = {
token: token,
state: urlParams.get('state') || undefined,
error: urlParams.get('error') || undefined,
error_description: urlParams.get('error_description') || undefined,
};
setCallbackData(data);
// If we have a token, try to verify it
if (data.token && !data.error) {
await verifyToken(data.token);
}
} catch (error) {
console.error('Error parsing callback data:', error);
setVerificationError('Failed to parse callback data');
} finally {
setLoading(false);
}
};
// Utility function to get cookie value by name
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const cookieValue = parts.pop()?.split(';').shift();
return cookieValue || null;
}
return null;
};
const verifyToken = async (token: string) => {
try {
// We need to extract app_id from the state or make a best guess
// For now, we'll try to verify without specifying app_id
// In a real implementation, the app_id should be included in the state parameter
// Try to get app_id from localStorage if it was stored during the test
const testData = localStorage.getItem('token_tester_data');
let appId = '';
if (testData) {
try {
const parsed = JSON.parse(testData);
appId = parsed.app_id || '';
} catch (e) {
console.warn('Could not parse stored test data');
}
}
if (!appId) {
// If we don't have app_id, we can't verify the token properly
setVerificationError('Cannot verify token: Application ID not found in callback state');
return;
}
const verifyRequest = {
app_id: appId,
token: token,
permissions: [], // We'll verify without specific permissions
};
const result = await apiService.verifyToken(verifyRequest);
setVerificationResult(result);
} catch (error: any) {
console.error('Token verification failed:', error);
setVerificationError(
error.response?.data?.message ||
error.message ||
'Token verification failed'
);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const goBackToTester = () => {
navigate('/token-tester');
};
if (loading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px'
}}>
<Spin size="large" />
<Text style={{ marginLeft: '16px' }}>Processing callback...</Text>
</div>
);
}
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Token Tester - Callback</Title>
<Text type="secondary">
Callback page for testing the login flow
</Text>
</div>
<Button
icon={<ArrowLeftOutlined />}
onClick={goBackToTester}
>
Back to Tester
</Button>
</div>
{/* Callback Status */}
<Card title="Callback Status">
{callbackData.error ? (
<Alert
message="Callback Error"
description={`${callbackData.error}: ${callbackData.error_description || 'No description provided'}`}
type="error"
showIcon
/>
) : callbackData.token ? (
<Alert
message="Callback Successful"
description="Token received successfully from the login flow"
type="success"
showIcon
/>
) : (
<Alert
message="Invalid Callback"
description="No token or error information found in callback URL"
type="warning"
showIcon
/>
)}
</Card>
{/* Token Information */}
{callbackData.token && (
<Card title="Received Token">
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Text strong>Token:</Text>
<TextArea
value={callbackData.token}
readOnly
rows={4}
style={{ fontFamily: 'monospace', fontSize: '12px', marginTop: '8px' }}
/>
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(callbackData.token!)}
style={{ marginTop: '8px' }}
>
Copy Token
</Button>
</div>
{callbackData.state && (
<div>
<Text strong>State:</Text>
<div style={{ marginTop: '4px' }}>
<Text code>{callbackData.state}</Text>
</div>
</div>
)}
</Space>
</Card>
)}
{/* Token Verification Results */}
{verificationResult && (
<Card
title={
<Space>
{verificationResult.valid ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)}
Token Verification Results
</Space>
}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
message={verificationResult.valid ? 'Token Valid' : 'Token Invalid'}
description={verificationResult.valid
? 'The token was successfully verified'
: verificationResult.error || 'Token verification failed'
}
type={verificationResult.valid ? 'success' : 'error'}
showIcon
/>
{verificationResult.valid && (
<div>
<Row gutter={16}>
<Col span={12}>
<Card size="small" title="Token Information">
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Text strong>Token Type:</Text>
<div>
<Tag color="blue">{verificationResult.token_type.toUpperCase()}</Tag>
</div>
</div>
{verificationResult.user_id && (
<div>
<Text strong>User ID:</Text>
<div>{verificationResult.user_id}</div>
</div>
)}
{verificationResult.expires_at && (
<div>
<Text strong>Expires At:</Text>
<div>{new Date(verificationResult.expires_at).toLocaleString()}</div>
</div>
)}
{verificationResult.max_valid_at && (
<div>
<Text strong>Max Valid Until:</Text>
<div>{new Date(verificationResult.max_valid_at).toLocaleString()}</div>
</div>
)}
</Space>
</Card>
</Col>
<Col span={12}>
<Card size="small" title="Permissions">
<Space direction="vertical" style={{ width: '100%' }}>
{verificationResult.permissions && verificationResult.permissions.length > 0 ? (
<div>
<Text strong>Available Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{verificationResult.permissions.map(permission => (
<Tag key={permission} color="green" style={{ margin: '2px' }}>
{permission}
</Tag>
))}
</div>
</div>
) : (
<Text type="secondary">No permissions available</Text>
)}
{verificationResult.permission_results && Object.keys(verificationResult.permission_results).length > 0 && (
<div style={{ marginTop: '16px' }}>
<Text strong>Permission Check Results:</Text>
<div style={{ marginTop: '8px' }}>
{Object.entries(verificationResult.permission_results).map(([permission, granted]) => (
<div key={permission} style={{ marginBottom: '4px' }}>
<Tag color={granted ? 'green' : 'red'}>
{permission}: {granted ? 'GRANTED' : 'DENIED'}
</Tag>
</div>
))}
</div>
</div>
)}
</Space>
</Card>
</Col>
</Row>
{verificationResult.claims && Object.keys(verificationResult.claims).length > 0 && (
<Card size="small" title="Token Claims" style={{ marginTop: '16px' }}>
<Row gutter={8}>
{Object.entries(verificationResult.claims).map(([key, value]) => (
<Col span={8} key={key} style={{ marginBottom: '8px' }}>
<Text strong>{key}:</Text>
<div>{value}</div>
</Col>
))}
</Row>
</Card>
)}
</div>
)}
</Space>
</Card>
)}
{/* Verification Error */}
{verificationError && (
<Card title="Verification Error">
<Alert
message="Token Verification Failed"
description={verificationError}
type="error"
showIcon
/>
</Card>
)}
{/* No Token or Error */}
{!callbackData.token && !callbackData.error && (
<Result
status="warning"
title="Invalid Callback"
subTitle="This callback page expects to receive either a token or error information from the login flow."
extra={
<Button type="primary" onClick={goBackToTester}>
Go Back to Token Tester
</Button>
}
/>
)}
</Space>
</div>
);
};
export default TokenTesterCallback;

View File

@ -1,811 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Typography,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Tag,
Card,
Row,
Col,
Alert,
Checkbox,
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
EyeOutlined,
CopyOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { apiService, Application, StaticToken, CreateTokenRequest, CreateTokenResponse, VerifyRequest } from '../services/apiService';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
const { Option } = Select;
const { TextArea } = Input;
interface TokenWithApp extends StaticToken {
app?: Application;
}
const availablePermissions = [
'app.read',
'app.write',
'app.delete',
'token.read',
'token.create',
'token.revoke',
'repo.read',
'repo.write',
'repo.admin',
'permission.read',
'permission.write',
'permission.grant',
'permission.revoke',
];
const Tokens: React.FC = () => {
const [tokens, setTokens] = useState<TokenWithApp[]>([]);
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [verifyModalVisible, setVerifyModalVisible] = useState(false);
const [tokenDetailsVisible, setTokenDetailsVisible] = useState(false);
const [selectedToken, setSelectedToken] = useState<TokenWithApp | null>(null);
const [newTokenResponse, setNewTokenResponse] = useState<CreateTokenResponse | null>(null);
const [verifyResult, setVerifyResult] = useState<any>(null);
const [verifyLoading, setVerifyLoading] = useState(false);
const [form] = Form.useForm();
const [verifyForm] = Form.useForm();
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications();
setApplications(response.data);
if (response.data.length > 0) {
loadAllTokens(response.data);
}
} catch (error) {
console.error('Failed to load applications:', error);
message.error('Failed to load applications');
} finally {
setLoading(false);
}
};
const loadAllTokens = async (apps: Application[]) => {
try {
const allTokens: TokenWithApp[] = [];
for (const app of apps) {
try {
const tokensResponse = await apiService.getTokensForApplication(app.app_id);
const tokensWithApp = tokensResponse.data.map(token => ({
...token,
app,
}));
allTokens.push(...tokensWithApp);
} catch (error) {
console.warn(`Failed to load tokens for app ${app.app_id}:`, error);
}
}
setTokens(allTokens);
} catch (error) {
console.error('Failed to load tokens:', error);
message.error('Failed to load tokens');
}
};
const handleCreate = () => {
form.resetFields();
setNewTokenResponse(null);
setModalVisible(true);
};
const handleDelete = async (tokenId: string) => {
try {
await apiService.deleteToken(tokenId);
message.success('Token deleted successfully');
loadApplications();
} catch (error) {
console.error('Failed to delete token:', error);
message.error('Failed to delete token');
}
};
const handleSubmit = async (values: any) => {
try {
// Debug logging to identify the issue
console.log('Form values:', values);
console.log('App ID:', values.app_id);
// More robust validation for app_id
if (!values.app_id || values.app_id === 'undefined' || values.app_id === undefined) {
console.error('Invalid app_id detected:', values.app_id);
message.error('Please select an application');
return;
}
// Validate that the app_id exists in our applications list
const selectedApp = applications.find(app => app.app_id === values.app_id);
if (!selectedApp) {
console.error('Selected app_id not found in applications list:', values.app_id);
message.error('Selected application is not valid. Please refresh and try again.');
return;
}
const requestData: CreateTokenRequest = {
owner: {
type: values.owner_type,
name: values.owner_name,
owner: values.owner_owner,
},
permissions: values.permissions,
};
console.log('Creating token for app:', values.app_id);
console.log('Request data:', requestData);
const response = await apiService.createToken(values.app_id, requestData);
console.log('Token creation response:', response);
setNewTokenResponse(response);
message.success('Token created successfully');
loadApplications();
} catch (error) {
console.error('Failed to create token:', error);
message.error('Failed to create token');
}
};
const handleOpenVerifyModal = () => {
verifyForm.resetFields();
setVerifyResult(null);
setVerifyLoading(false);
setVerifyModalVisible(true);
};
const handleVerifyToken = async (values: any) => {
try {
setVerifyLoading(true);
const verifyRequest: VerifyRequest = {
app_id: values.app_id,
// Remove explicit type - it will be auto-detected from token prefix
token: values.token,
permissions: values.permissions || [],
};
console.log('Verifying token with request:', verifyRequest);
const response = await apiService.verifyToken(verifyRequest);
console.log('Token verification response:', response);
// Store the result in state to display in the modal
setVerifyResult(response);
// Show success message
if (response && response.valid) {
message.success('Token verification completed successfully!', 3);
} else {
message.warning('Token verification completed - token is invalid', 3);
}
} catch (error) {
console.error('Failed to verify token:', error);
// Store error result in state
setVerifyResult({
valid: false,
error: error instanceof Error ? error.message : 'An unexpected error occurred while verifying the token.',
errorDetails: {
networkError: true,
suggestions: [
'Check your network connection',
'Verify the token format is correct',
'Ensure the selected application is correct',
'Confirm the API server is running'
]
}
});
message.error('Failed to verify token. Please check your network connection and try again.');
} finally {
setVerifyLoading(false);
}
};
const showTokenDetails = (token: TokenWithApp) => {
setSelectedToken(token);
setTokenDetailsVisible(true);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('Copied to clipboard');
};
const columns = [
{
title: 'Token ID',
dataIndex: 'id',
key: 'id',
render: (text: string) => <Text code>{text.substring(0, 8)}...</Text>,
},
{
title: 'Application',
dataIndex: 'app',
key: 'app',
render: (app: Application) => (
<div>
<Text strong>{app?.app_id || 'Unknown'}</Text>
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
{app?.app_link || ''}
</Text>
</div>
),
},
{
title: 'Owner',
dataIndex: 'owner',
key: 'owner',
render: (owner: StaticToken['owner']) => (
<div>
<div>{owner.name}</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{owner.type} {owner.owner}
</Text>
</div>
),
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<Tag color="blue">{type.toUpperCase()}</Tag>
),
},
{
title: 'Created',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => dayjs(date).format('MMM DD, YYYY'),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: TokenWithApp) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showTokenDetails(record)}
title="View Details"
/>
<Popconfirm
title="Are you sure you want to delete this token?"
onConfirm={() => handleDelete(record.id)}
okText="Yes"
cancelText="No"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
title="Delete"
/>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Tokens</Title>
<Text type="secondary">
Manage static tokens for your applications
</Text>
</div>
<Space>
<Button
icon={<CheckCircleOutlined />}
onClick={handleOpenVerifyModal}
>
Verify Token
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
Create Token
</Button>
</Space>
</div>
<Table
columns={columns}
dataSource={tokens}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} tokens`,
}}
/>
</Space>
{/* Create Token Modal */}
<Modal
title="Create Static Token"
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={() => form.submit()}
width={600}
>
{newTokenResponse ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Token Created Successfully"
description="Please copy and save this token securely. It will not be shown again."
type="success"
showIcon
/>
<Card title="New Token Details">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text strong>Token ID:</Text>
<div>
<Text code>{newTokenResponse.id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(newTokenResponse.id)}
/>
</div>
</div>
<div>
<Text strong>Token:</Text>
<div>
<TextArea
value={newTokenResponse.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace' }}
/>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(newTokenResponse.token)}
style={{ marginTop: '8px' }}
>
Copy Token
</Button>
</div>
</div>
<div>
<Text strong>Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{newTokenResponse.permissions.map(permission => (
<Tag key={permission} color="blue">{permission}</Tag>
))}
</div>
</div>
<div>
<Text strong>Created:</Text>
<div>{dayjs(newTokenResponse.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</div>
</Space>
</Card>
</Space>
) : (
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="app_id"
label="Application"
rules={[{ required: true, message: 'Please select an application' }]}
>
<Select placeholder="Select application">
{applications.map(app => (
<Option key={app.app_id} value={app.app_id}>
{app.app_id}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="permissions"
label="Permissions"
rules={[{ required: true, message: 'Please select at least one permission' }]}
>
<Checkbox.Group>
<Row>
{availablePermissions.map(permission => (
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
<Checkbox value={permission}>{permission}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
name="owner_type"
label="Owner Type"
rules={[{ required: true, message: 'Please select owner type' }]}
>
<Select placeholder="Select owner type">
<Option value="individual">Individual</Option>
<Option value="team">Team</Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="owner_name"
label="Owner Name"
rules={[{ required: true, message: 'Please enter owner name' }]}
>
<Input placeholder="John Doe" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="owner_owner"
label="Owner Contact"
rules={[{ required: true, message: 'Please enter owner contact' }]}
>
<Input placeholder="john.doe@example.com" />
</Form.Item>
</Col>
</Row>
</Form>
)}
</Modal>
{/* Verify Token Modal */}
<Modal
title="Verify Token"
open={verifyModalVisible}
onCancel={() => setVerifyModalVisible(false)}
onOk={() => verifyForm.submit()}
confirmLoading={verifyLoading}
width={800}
>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Automatic Token Type Detection"
description="The system will automatically detect if your token is a static token (KMST-, KMS2T-, etc.) or user token (KMSUT-, KMS2UT-, etc.) based on its prefix."
type="info"
showIcon
/>
<Form
form={verifyForm}
layout="vertical"
onFinish={handleVerifyToken}
>
<Form.Item
name="app_id"
label="Application"
rules={[{ required: true, message: 'Please select an application' }]}
>
<Select placeholder="Select application">
{applications.map(app => (
<Option key={app.app_id} value={app.app_id}>
{app.app_id}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="token"
label="Token"
rules={[{ required: true, message: 'Please enter the token to verify' }]}
>
<TextArea
placeholder="Enter the token to verify (paste from Token Tester for user tokens, or from static token creation)"
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
<Form.Item
name="permissions"
label="Permissions to Check (Optional)"
>
<Checkbox.Group>
<Row>
{availablePermissions.map(permission => (
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
<Checkbox value={permission}>{permission}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
</Form>
{/* Verification Results */}
{verifyResult && (
<div>
<Title level={4} style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{verifyResult.valid ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)}
Verification Results
</div>
</Title>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* Overall Status */}
<Card size="small" style={{ backgroundColor: verifyResult.valid ? '#f6ffed' : '#fff2f0' }}>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div>
<Text strong>Token Status: </Text>
{verifyResult.valid ? (
<Tag color="green" icon={<CheckCircleOutlined />} style={{ fontSize: '14px' }}>
VALID
</Tag>
) : (
<Tag color="red" icon={<ExclamationCircleOutlined />} style={{ fontSize: '14px' }}>
INVALID
</Tag>
)}
</div>
{verifyResult.permitted !== undefined && (
<div>
<Text strong>Permission Status: </Text>
{verifyResult.permitted ? (
<Tag color="green" icon={<CheckCircleOutlined />} style={{ fontSize: '14px' }}>
ALL PERMISSIONS GRANTED
</Tag>
) : (
<Tag color="orange" icon={<ExclamationCircleOutlined />} style={{ fontSize: '14px' }}>
SOME PERMISSIONS DENIED
</Tag>
)}
</div>
)}
{verifyResult.token_type && (
<div>
<Text strong>Token Type: </Text>
<Tag color="blue" style={{ fontSize: '14px' }}>
{verifyResult.token_type.toUpperCase()}
</Tag>
</div>
)}
</Space>
</Card>
{/* Token Permissions */}
{verifyResult.permissions && verifyResult.permissions.length > 0 && (
<div>
<Text strong style={{ fontSize: '16px' }}>Available Token Permissions:</Text>
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#fafafa', borderRadius: '6px' }}>
<Space wrap>
{verifyResult.permissions.map((permission: string) => (
<Tag key={permission} color="blue" style={{ margin: '2px' }}>
{permission}
</Tag>
))}
</Space>
</div>
</div>
)}
{/* Requested Permission Results */}
{verifyResult.permission_results && Object.keys(verifyResult.permission_results).length > 0 && (
<div>
<Text strong style={{ fontSize: '16px' }}>Requested Permission Results:</Text>
<div style={{ marginTop: '12px' }}>
{Object.entries(verifyResult.permission_results).map(([permission, granted]) => (
<div key={permission} style={{
marginBottom: '8px',
padding: '8px 12px',
backgroundColor: granted ? '#f6ffed' : '#fff2f0',
borderRadius: '4px',
border: `1px solid ${granted ? '#b7eb8f' : '#ffccc7'}`
}}>
<Space>
{granted ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)}
<Text strong>{permission}</Text>
<Tag color={granted ? 'green' : 'red'} style={{ marginLeft: 'auto' }}>
{granted ? 'GRANTED' : 'DENIED'}
</Tag>
</Space>
</div>
))}
</div>
</div>
)}
{/* Error Information */}
{verifyResult.error && (
<Alert
message="Verification Error"
description={verifyResult.error}
type="error"
showIcon
style={{ marginTop: '16px' }}
/>
)}
{/* Error Details with Suggestions */}
{verifyResult.errorDetails && verifyResult.errorDetails.networkError && (
<div>
<Text type="secondary">
Please check:
</Text>
<ul style={{ marginTop: '8px', paddingLeft: '20px' }}>
{verifyResult.errorDetails.suggestions.map((suggestion: string, index: number) => (
<li key={index}>{suggestion}</li>
))}
</ul>
</div>
)}
{/* Additional Information */}
{(verifyResult.expires_at || verifyResult.max_valid_at) && (
<div>
<Text strong style={{ fontSize: '16px' }}>Token Timing Information:</Text>
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#fafafa', borderRadius: '6px' }}>
{verifyResult.expires_at && (
<div style={{ marginBottom: '8px' }}>
<Text strong>Expires At: </Text>
<Text code>{new Date(verifyResult.expires_at).toLocaleString()}</Text>
</div>
)}
{verifyResult.max_valid_at && (
<div>
<Text strong>Max Valid Until: </Text>
<Text code>{new Date(verifyResult.max_valid_at).toLocaleString()}</Text>
</div>
)}
</div>
</div>
)}
</Space>
</div>
)}
</Space>
</Modal>
{/* Token Details Modal */}
<Modal
title="Token Details"
open={tokenDetailsVisible}
onCancel={() => setTokenDetailsVisible(false)}
footer={[
<Button key="close" onClick={() => setTokenDetailsVisible(false)}>
Close
</Button>,
]}
width={600}
>
{selectedToken && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Token Information">
<Row gutter={16}>
<Col span={12}>
<Text strong>Token ID:</Text>
<div>
<Text code>{selectedToken.id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedToken.id)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>Type:</Text>
<div>
<Tag color="blue">{selectedToken.type.toUpperCase()}</Tag>
</div>
</Col>
</Row>
</Card>
<Card title="Application">
<Row gutter={16}>
<Col span={12}>
<Text strong>App ID:</Text>
<div>
<Text code>{selectedToken.app?.app_id}</Text>
</div>
</Col>
<Col span={12}>
<Text strong>App Link:</Text>
<div>
<a href={selectedToken.app?.app_link} target="_blank" rel="noopener noreferrer">
{selectedToken.app?.app_link}
</a>
</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedToken.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedToken.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedToken.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedToken.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedToken.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedToken.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Tokens;

View File

@ -1,416 +0,0 @@
import React, { useState } from 'react';
import {
Card,
Typography,
Space,
Form,
Input,
Button,
Select,
Alert,
Row,
Col,
Tag,
Modal,
message,
} from 'antd';
import {
UserOutlined,
LoginOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { apiService } from '../services/apiService';
import { useAuth } from '../contexts/AuthContext';
const { Title, Text } = Typography;
const { Option } = Select;
const Users: React.FC = () => {
const { user } = useAuth();
const [loginForm] = Form.useForm();
const [renewForm] = Form.useForm();
const [loginModalVisible, setLoginModalVisible] = useState(false);
const [renewModalVisible, setRenewModalVisible] = useState(false);
const [loginResult, setLoginResult] = useState<any>(null);
const [renewResult, setRenewResult] = useState<any>(null);
const handleUserLogin = async (values: any) => {
try {
const response = await apiService.login(
values.app_id,
values.permissions || [],
values.redirect_uri
);
setLoginResult(response);
message.success('User login initiated successfully');
} catch (error) {
console.error('Failed to initiate user login:', error);
message.error('Failed to initiate user login');
}
};
const handleTokenRenewal = async (values: any) => {
try {
const response = await apiService.renewToken(
values.app_id,
values.user_id,
values.token
);
setRenewResult(response);
message.success('Token renewed successfully');
} catch (error) {
console.error('Failed to renew token:', error);
message.error('Failed to renew token');
}
};
const availablePermissions = [
'app.read',
'app.write',
'app.delete',
'token.read',
'token.create',
'token.revoke',
'repo.read',
'repo.write',
'repo.admin',
'permission.read',
'permission.write',
'permission.grant',
'permission.revoke',
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={2}>User Management</Title>
<Text type="secondary">
Manage user authentication and token operations
</Text>
</div>
{/* Current User Info */}
<Card title="Current User">
<Row gutter={16}>
<Col span={8}>
<Text strong>Email:</Text>
<div>{user?.email}</div>
</Col>
<Col span={8}>
<Text strong>Status:</Text>
<div>
<Tag color="green" icon={<CheckCircleOutlined />}>
AUTHENTICATED
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{user?.permissions.map(permission => (
<Tag key={permission} color="blue" style={{ marginBottom: '4px' }}>
{permission}
</Tag>
))}
</div>
</Col>
</Row>
</Card>
{/* User Operations */}
<Row gutter={16}>
<Col span={12}>
<Card
title="User Login"
extra={
<Button
type="primary"
icon={<LoginOutlined />}
onClick={() => setLoginModalVisible(true)}
>
Initiate Login
</Button>
}
>
<Text type="secondary">
Initiate a user authentication flow for an application. This will generate
a user token that can be used for API access with specific permissions.
</Text>
</Card>
</Col>
<Col span={12}>
<Card
title="Token Renewal"
extra={
<Button
icon={<ClockCircleOutlined />}
onClick={() => setRenewModalVisible(true)}
>
Renew Token
</Button>
}
>
<Text type="secondary">
Renew an existing user token to extend its validity period.
This is useful for maintaining long-running sessions.
</Text>
</Card>
</Col>
</Row>
{/* Information Cards */}
<Row gutter={16}>
<Col span={8}>
<Card>
<div style={{ textAlign: 'center' }}>
<UserOutlined style={{ fontSize: '32px', color: '#1890ff', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>1</div>
<div>Active Users</div>
</div>
</Card>
</Col>
<Col span={8}>
<Card>
<div style={{ textAlign: 'center' }}>
<LoginOutlined style={{ fontSize: '32px', color: '#52c41a', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>Demo</div>
<div>Authentication Mode</div>
</div>
</Card>
</Col>
<Col span={8}>
<Card>
<div style={{ textAlign: 'center' }}>
<CheckCircleOutlined style={{ fontSize: '32px', color: '#722ed1', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>Active</div>
<div>Session Status</div>
</div>
</Card>
</Col>
</Row>
{/* Authentication Flow Information */}
<Card title="Authentication Flow Information">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
message="Demo Mode"
description="This frontend is running in demo mode. In a production environment, user authentication would integrate with your identity provider (OAuth2, SAML, etc.)."
type="info"
showIcon
/>
<div>
<Title level={4}>Supported Authentication Methods</Title>
<ul>
<li><strong>Header Authentication:</strong> Uses X-User-Email header for demo purposes</li>
<li><strong>OAuth2:</strong> Standard OAuth2 flow with authorization code grant</li>
<li><strong>SAML:</strong> SAML 2.0 single sign-on integration</li>
<li><strong>JWT:</strong> JSON Web Token based authentication</li>
</ul>
</div>
<div>
<Title level={4}>Token Types</Title>
<ul>
<li><strong>User Tokens:</strong> Short-lived tokens for authenticated users</li>
<li><strong>Static Tokens:</strong> Long-lived tokens for service-to-service communication</li>
<li><strong>Renewal Tokens:</strong> Used to extend user token validity</li>
</ul>
</div>
</Space>
</Card>
</Space>
{/* User Login Modal */}
<Modal
title="Initiate User Login"
open={loginModalVisible}
onCancel={() => {
setLoginModalVisible(false);
setLoginResult(null);
}}
onOk={() => loginForm.submit()}
width={600}
>
{loginResult ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Login Flow Initiated"
description="The user login flow has been initiated successfully."
type="success"
showIcon
/>
<Card title="Login Response">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{loginResult.redirect_url && (
<div>
<Text strong>Redirect URL:</Text>
<div>
<a href={loginResult.redirect_url} target="_blank" rel="noopener noreferrer">
{loginResult.redirect_url}
</a>
</div>
</div>
)}
{loginResult.token && (
<div>
<Text strong>Token:</Text>
<div>
<Input.TextArea
value={loginResult.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</div>
</div>
)}
{loginResult.user_id && (
<div>
<Text strong>User ID:</Text>
<div>{loginResult.user_id}</div>
</div>
)}
{loginResult.expires_in && (
<div>
<Text strong>Expires In:</Text>
<div>{loginResult.expires_in} seconds</div>
</div>
)}
</Space>
</Card>
</Space>
) : (
<Form
form={loginForm}
layout="vertical"
onFinish={handleUserLogin}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" />
</Form.Item>
<Form.Item
name="permissions"
label="Requested Permissions"
>
<Select
mode="multiple"
placeholder="Select permissions"
allowClear
>
{availablePermissions.map(permission => (
<Option key={permission} value={permission}>
{permission}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="redirect_uri"
label="Redirect URI (Optional)"
>
<Input placeholder="https://example.com/callback" />
</Form.Item>
</Form>
)}
</Modal>
{/* Token Renewal Modal */}
<Modal
title="Renew User Token"
open={renewModalVisible}
onCancel={() => {
setRenewModalVisible(false);
setRenewResult(null);
}}
onOk={() => renewForm.submit()}
width={600}
>
{renewResult ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Token Renewed Successfully"
description="The user token has been renewed with extended validity."
type="success"
showIcon
/>
<Card title="Renewal Response">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text strong>New Token:</Text>
<div>
<Input.TextArea
value={renewResult.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</div>
</div>
<div>
<Text strong>Expires At:</Text>
<div>{new Date(renewResult.expires_at).toLocaleString()}</div>
</div>
<div>
<Text strong>Max Valid At:</Text>
<div>{new Date(renewResult.max_valid_at).toLocaleString()}</div>
</div>
</Space>
</Card>
</Space>
) : (
<Form
form={renewForm}
layout="vertical"
onFinish={handleTokenRenewal}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" />
</Form.Item>
<Form.Item
name="user_id"
label="User ID"
rules={[{ required: true, message: 'Please enter user ID' }]}
>
<Input placeholder="user@example.com" />
</Form.Item>
<Form.Item
name="token"
label="Current Token"
rules={[{ required: true, message: 'Please enter current token' }]}
>
<Input.TextArea
placeholder="Enter the current user token"
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
</Form>
)}
</Modal>
</div>
);
};
export default Users;

View File

@ -1,95 +0,0 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { message } from 'antd';
import { apiService } from '../services/apiService';
interface User {
email: string;
permissions: string[];
}
interface AuthContextType {
user: User | null;
login: (email: string) => Promise<boolean>;
logout: () => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is already logged in (from localStorage)
const savedUser = localStorage.getItem('kms_user');
if (savedUser) {
try {
const parsedUser = JSON.parse(savedUser);
setUser(parsedUser);
} catch (error) {
console.error('Error parsing saved user:', error);
localStorage.removeItem('kms_user');
}
}
setLoading(false);
}, []);
const login = async (email: string): Promise<boolean> => {
try {
setLoading(true);
// Test API connectivity with health check
await apiService.healthCheck();
// For demo purposes, we'll simulate login with the provided email
// In a real implementation, this would involve proper authentication
const userData: User = {
email,
permissions: ['app.read', 'app.write', 'token.read', 'token.create', 'token.revoke']
};
setUser(userData);
localStorage.setItem('kms_user', JSON.stringify(userData));
message.success('Login successful!');
return true;
} catch (error) {
console.error('Login error:', error);
message.error('Login failed. Please check your connection and try again.');
return false;
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
localStorage.removeItem('kms_user');
message.success('Logged out successfully');
};
const value: AuthContextType = {
user,
login,
logout,
loading,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

View File

@ -1,120 +0,0 @@
import React from 'react';
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
import { Layout, Menu, theme } from 'antd';
import {
DashboardOutlined,
AppstoreOutlined,
KeyOutlined,
UserOutlined,
AuditOutlined,
ExperimentOutlined,
} from '@ant-design/icons';
import { useState, useEffect } from 'react';
import Dashboard from '../components/Dashboard';
import Applications from '../components/Applications';
import Tokens from '../components/Tokens';
import Users from '../components/Users';
import Audit from '../components/Audit';
import TokenTester from '../components/TokenTester';
import TokenTesterCallback from '../components/TokenTesterCallback';
import { AuthProvider, useAuth } from '../contexts/AuthContext';
import Login from '../components/Login';
const { Sider, Content } = Layout;
const KMSAppContent: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const { user } = useAuth();
const navigate = useNavigate();
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
if (!user) {
return <Login />;
}
const menuItems = [
{
key: '/kms',
icon: <DashboardOutlined />,
label: 'Dashboard',
},
{
key: '/kms/applications',
icon: <AppstoreOutlined />,
label: 'Applications',
},
{
key: '/kms/tokens',
icon: <KeyOutlined />,
label: 'Tokens',
},
{
key: '/kms/token-tester',
icon: <ExperimentOutlined />,
label: 'Token Tester',
},
{
key: '/kms/users',
icon: <UserOutlined />,
label: 'Users',
},
{
key: '/kms/audit',
icon: <AuditOutlined />,
label: 'Audit Log',
},
];
return (
<Layout style={{ height: '100%', minHeight: '600px' }}>
<Sider trigger={null} collapsible collapsed={collapsed} width={200}>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['/kms']}
items={menuItems}
onClick={({ key }) => {
navigate(key);
}}
style={{ height: '100%', borderRight: 0 }}
/>
</Sider>
<Layout style={{ background: colorBgContainer }}>
<Content
style={{
margin: '16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
borderRadius: borderRadiusLG,
overflow: 'auto',
}}
>
<Routes>
<Route path="/kms" element={<Dashboard />} />
<Route path="/kms/applications" element={<Applications />} />
<Route path="/kms/tokens" element={<Tokens />} />
<Route path="/kms/token-tester" element={<TokenTester />} />
<Route path="/kms/token-tester/callback" element={<TokenTesterCallback />} />
<Route path="/kms/users" element={<Users />} />
<Route path="/kms/audit" element={<Audit />} />
<Route path="*" element={<Navigate to="/kms" replace />} />
</Routes>
</Content>
</Layout>
</Layout>
);
};
const KMSApp: React.FC = () => {
return (
<AuthProvider>
<KMSAppContent />
</AuthProvider>
);
};
export default KMSApp;

View File

@ -1,64 +0,0 @@
import { apiService, Application, PaginatedResponse } from '../services/apiService';
interface SearchResult {
id: string;
title: string;
description?: string;
appId: string;
action?: () => void;
}
export const kmsSearchProvider = async (query: string): Promise<SearchResult[]> => {
const results: SearchResult[] = [];
try {
// Search applications
const applicationsResponse: PaginatedResponse<Application> = await apiService.getApplications();
applicationsResponse.data
.filter((app: Application) =>
app.app_id.toLowerCase().includes(query.toLowerCase()) ||
(app.owner?.name && app.owner.name.toLowerCase().includes(query.toLowerCase()))
)
.forEach((app: Application) => {
results.push({
id: `app-${app.app_id}`,
title: app.app_id,
description: `Application owned by ${app.owner?.name || 'Unknown'}`,
appId: 'KMS',
action: () => {
window.location.hash = '/kms/applications';
}
});
});
// Add quick actions
const quickActions = [
{ key: 'applications', title: 'Applications', path: '/kms/applications' },
{ key: 'tokens', title: 'Tokens', path: '/kms/tokens' },
{ key: 'users', title: 'Users', path: '/kms/users' },
{ key: 'audit', title: 'Audit Log', path: '/kms/audit' },
{ key: 'dashboard', title: 'Dashboard', path: '/kms' },
];
quickActions
.filter(action => action.title.toLowerCase().includes(query.toLowerCase()))
.forEach(action => {
results.push({
id: `quick-${action.key}`,
title: action.title,
description: `Navigate to ${action.title}`,
appId: 'KMS',
action: () => {
window.location.hash = action.path;
}
});
});
} catch (error) {
console.error('KMS search error:', error);
}
return results.slice(0, 10); // Limit results
};
export default kmsSearchProvider;

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,3 +0,0 @@
import('./bootstrap');
export {};

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,311 +0,0 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
// Types based on the KMS API
export interface Application {
app_id: string;
app_link: string;
type: string[];
callback_url: string;
hmac_key: string;
token_prefix?: string;
token_renewal_duration: number;
max_token_duration: number;
owner: {
type: string;
name: string;
owner: string;
};
created_at: string;
updated_at: string;
}
export interface StaticToken {
id: string;
app_id: string;
owner: {
type: string;
name: string;
owner: string;
};
type: string;
created_at: string;
updated_at: string;
}
export interface CreateApplicationRequest {
app_id: string;
app_link: string;
type: string[];
callback_url: string;
token_prefix?: string;
token_renewal_duration: string;
max_token_duration: string;
owner: {
type: string;
name: string;
owner: string;
};
}
export interface CreateTokenRequest {
owner: {
type: string;
name: string;
owner: string;
};
permissions: string[];
}
export interface CreateTokenResponse {
id: string;
token: string;
permissions: string[];
created_at: string;
}
export interface PaginatedResponse<T> {
data: T[];
limit: number;
offset: number;
count: number;
}
export interface VerifyRequest {
app_id: string;
user_id?: string;
token: string;
permissions?: string[];
}
export interface VerifyResponse {
valid: boolean;
permitted: boolean;
user_id?: string;
permissions: string[];
permission_results?: Record<string, boolean>;
expires_at?: string;
max_valid_at?: string;
token_type: string;
claims?: Record<string, string>;
error?: string;
}
export interface AuditEvent {
id: string;
type: string;
status: string;
timestamp: string;
actor_id?: string;
actor_ip?: string;
user_agent?: string;
resource_id?: string;
resource_type?: string;
action: string;
description: string;
details?: Record<string, any>;
request_id?: string;
session_id?: string;
}
export interface AuditQueryParams {
event_types?: string[];
statuses?: string[];
actor_id?: string;
resource_id?: string;
resource_type?: string;
start_time?: string;
end_time?: string;
limit?: number;
offset?: number;
order_by?: string;
order_desc?: boolean;
}
export interface AuditResponse {
events: AuditEvent[];
total: number;
limit: number;
offset: number;
}
export interface AuditStats {
total_events: number;
by_type: Record<string, number>;
by_severity: Record<string, number>;
by_status: Record<string, number>;
by_time?: Record<string, number>;
}
export interface AuditStatsParams {
event_types?: string[];
start_time?: string;
end_time?: string;
group_by?: string;
}
class ApiService {
private api: AxiosInstance;
private baseURL: string;
constructor() {
this.baseURL = process.env.REACT_APP_API_URL || 'http://localhost:8080';
this.api = axios.create({
baseURL: this.baseURL,
headers: {
'Content-Type': 'application/json',
},
});
// Add request interceptor to include user email header
this.api.interceptors.request.use((config) => {
const user = localStorage.getItem('kms_user');
if (user) {
try {
const userData = JSON.parse(user);
config.headers['X-User-Email'] = userData.email;
} catch (error) {
console.error('Error parsing user data:', error);
}
}
return config;
});
// Add response interceptor for error handling
this.api.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error);
return Promise.reject(error);
}
);
}
// Health Check
async healthCheck(): Promise<any> {
const response = await this.api.get('/health');
return response.data;
}
async readinessCheck(): Promise<any> {
const response = await this.api.get('/ready');
return response.data;
}
// Applications
async getApplications(limit: number = 50, offset: number = 0): Promise<PaginatedResponse<Application>> {
const response = await this.api.get(`/api/applications?limit=${limit}&offset=${offset}`);
return response.data;
}
async getApplication(appId: string): Promise<Application> {
const response = await this.api.get(`/api/applications/${appId}`);
return response.data;
}
async createApplication(data: CreateApplicationRequest): Promise<Application> {
const response = await this.api.post('/api/applications', data);
return response.data;
}
async updateApplication(appId: string, data: Partial<CreateApplicationRequest>): Promise<Application> {
const response = await this.api.put(`/api/applications/${appId}`, data);
return response.data;
}
async deleteApplication(appId: string): Promise<void> {
await this.api.delete(`/api/applications/${appId}`);
}
// Tokens
async getTokensForApplication(appId: string, limit: number = 50, offset: number = 0): Promise<PaginatedResponse<StaticToken>> {
const response = await this.api.get(`/api/applications/${appId}/tokens?limit=${limit}&offset=${offset}`);
return response.data;
}
async createToken(appId: string, data: CreateTokenRequest): Promise<CreateTokenResponse> {
const response = await this.api.post(`/api/applications/${appId}/tokens`, data);
return response.data;
}
async deleteToken(tokenId: string): Promise<void> {
await this.api.delete(`/api/tokens/${tokenId}`);
}
// Token verification
async verifyToken(data: VerifyRequest): Promise<VerifyResponse> {
const response = await this.api.post('/api/verify', data);
return response.data;
}
// Authentication
async login(appId: string, permissions: string[], redirectUri?: string, tokenDelivery?: string): Promise<any> {
const response = await this.api.post('/api/login', {
app_id: appId,
permissions,
redirect_uri: redirectUri,
token_delivery: tokenDelivery,
});
return response.data;
}
async renewToken(appId: string, userId: string, token: string): Promise<any> {
const response = await this.api.post('/api/renew', {
app_id: appId,
user_id: userId,
token,
});
return response.data;
}
// Audit endpoints
async getAuditEvents(params?: AuditQueryParams): Promise<AuditResponse> {
const queryString = new URLSearchParams();
if (params) {
if (params.event_types?.length) {
params.event_types.forEach(type => queryString.append('event_types', type));
}
if (params.statuses?.length) {
params.statuses.forEach(status => queryString.append('statuses', status));
}
if (params.actor_id) queryString.set('actor_id', params.actor_id);
if (params.resource_id) queryString.set('resource_id', params.resource_id);
if (params.resource_type) queryString.set('resource_type', params.resource_type);
if (params.start_time) queryString.set('start_time', params.start_time);
if (params.end_time) queryString.set('end_time', params.end_time);
if (params.limit) queryString.set('limit', params.limit.toString());
if (params.offset) queryString.set('offset', params.offset.toString());
if (params.order_by) queryString.set('order_by', params.order_by);
if (params.order_desc !== undefined) queryString.set('order_desc', params.order_desc.toString());
}
const url = `/api/audit/events${queryString.toString() ? '?' + queryString.toString() : ''}`;
const response = await this.api.get(url);
return response.data;
}
async getAuditEvent(eventId: string): Promise<AuditEvent> {
const response = await this.api.get(`/api/audit/events/${eventId}`);
return response.data;
}
async getAuditStats(params?: AuditStatsParams): Promise<AuditStats> {
const queryString = new URLSearchParams();
if (params) {
if (params.event_types?.length) {
params.event_types.forEach(type => queryString.append('event_types', type));
}
if (params.start_time) queryString.set('start_time', params.start_time);
if (params.end_time) queryString.set('end_time', params.end_time);
if (params.group_by) queryString.set('group_by', params.group_by);
}
const url = `/api/audit/stats${queryString.toString() ? '?' + queryString.toString() : ''}`;
const response = await this.api.get(url);
return response.data;
}
}
export const apiService = new ApiService();

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}