remove old frontend
This commit is contained in:
@ -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
|
|
||||||
24
kms/kms-frontend/.gitignore
vendored
24
kms/kms-frontend/.gitignore
vendored
@ -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*
|
|
||||||
@ -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;"]
|
|
||||||
@ -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
|
|
||||||
@ -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",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
33492
kms/kms-frontend/package-lock.json
generated
33492
kms/kms-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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 |
@ -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 |
@ -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"
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import('./bootstrap');
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@ -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 |
1
kms/kms-frontend/src/react-app-env.d.ts
vendored
1
kms/kms-frontend/src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="react-scripts" />
|
|
||||||
@ -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;
|
|
||||||
@ -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();
|
|
||||||
@ -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';
|
|
||||||
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user