This commit is contained in:
2025-08-22 19:19:16 -04:00
parent df567983c1
commit 93831d4fad
36 changed files with 22412 additions and 29 deletions

View File

@ -0,0 +1,47 @@
package domain
import (
"encoding/json"
"fmt"
"time"
)
// Duration is a wrapper around time.Duration that can unmarshal from both
// string duration formats (like "168h") and nanosecond integers
type Duration struct {
time.Duration
}
// UnmarshalJSON implements json.Unmarshaler interface
func (d *Duration) UnmarshalJSON(data []byte) error {
// Try to unmarshal as string first (e.g., "168h", "24h", "30m")
var str string
if err := json.Unmarshal(data, &str); err == nil {
duration, err := time.ParseDuration(str)
if err != nil {
return fmt.Errorf("invalid duration format: %s", str)
}
d.Duration = duration
return nil
}
// Try to unmarshal as integer (nanoseconds)
var ns int64
if err := json.Unmarshal(data, &ns); err == nil {
d.Duration = time.Duration(ns)
return nil
}
return fmt.Errorf("duration must be either a string (e.g., '168h') or integer nanoseconds")
}
// MarshalJSON implements json.Marshaler interface
func (d Duration) MarshalJSON() ([]byte, error) {
// Always marshal as nanoseconds for consistency
return json.Marshal(int64(d.Duration))
}
// String returns the string representation of the duration
func (d Duration) String() string {
return d.Duration.String()
}

View File

@ -44,8 +44,8 @@ type Application struct {
Type []ApplicationType `json:"type" validate:"required,min=1,dive,oneof=static user" db:"type"`
CallbackURL string `json:"callback_url" validate:"required,url,max=500" db:"callback_url"`
HMACKey string `json:"hmac_key" validate:"required,min=1,max=255" db:"hmac_key"`
TokenRenewalDuration time.Duration `json:"token_renewal_duration" validate:"required,min=1" db:"token_renewal_duration"`
MaxTokenDuration time.Duration `json:"max_token_duration" validate:"required,min=1" db:"max_token_duration"`
TokenRenewalDuration Duration `json:"token_renewal_duration" validate:"required,min=1" db:"token_renewal_duration"`
MaxTokenDuration Duration `json:"max_token_duration" validate:"required,min=1" db:"max_token_duration"`
Owner Owner `json:"owner" validate:"required"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
@ -157,8 +157,8 @@ type CreateApplicationRequest struct {
AppLink string `json:"app_link" validate:"required,url,max=500"`
Type []ApplicationType `json:"type" validate:"required,min=1,dive,oneof=static user"`
CallbackURL string `json:"callback_url" validate:"required,url,max=500"`
TokenRenewalDuration time.Duration `json:"token_renewal_duration" validate:"required,min=1"`
MaxTokenDuration time.Duration `json:"max_token_duration" validate:"required,min=1"`
TokenRenewalDuration Duration `json:"token_renewal_duration" validate:"required,min=1"`
MaxTokenDuration Duration `json:"max_token_duration" validate:"required,min=1"`
Owner Owner `json:"owner" validate:"required"`
}
@ -168,8 +168,8 @@ type UpdateApplicationRequest struct {
Type *[]ApplicationType `json:"type,omitempty" validate:"omitempty,min=1,dive,oneof=static user"`
CallbackURL *string `json:"callback_url,omitempty" validate:"omitempty,url,max=500"`
HMACKey *string `json:"hmac_key,omitempty" validate:"omitempty,min=1,max=255"`
TokenRenewalDuration *time.Duration `json:"token_renewal_duration,omitempty" validate:"omitempty,min=1"`
MaxTokenDuration *time.Duration `json:"max_token_duration,omitempty" validate:"omitempty,min=1"`
TokenRenewalDuration *Duration `json:"token_renewal_duration,omitempty" validate:"omitempty,min=1"`
MaxTokenDuration *Duration `json:"max_token_duration,omitempty" validate:"omitempty,min=1"`
Owner *Owner `json:"owner,omitempty" validate:"omitempty"`
}

View File

@ -38,7 +38,7 @@ type TenantSettings struct {
OAuth2Settings *OAuth2Settings `json:"oauth2_settings,omitempty"`
// Session settings
SessionTimeout time.Duration `json:"session_timeout,omitempty"`
SessionTimeout Duration `json:"session_timeout,omitempty"`
MaxConcurrentSessions int `json:"max_concurrent_sessions,omitempty"`
// Security settings
@ -47,8 +47,8 @@ type TenantSettings struct {
PasswordPolicy *PasswordPolicy `json:"password_policy,omitempty"`
// Token settings
DefaultTokenDuration time.Duration `json:"default_token_duration,omitempty"`
MaxTokenDuration time.Duration `json:"max_token_duration,omitempty"`
DefaultTokenDuration Duration `json:"default_token_duration,omitempty"`
MaxTokenDuration Duration `json:"max_token_duration,omitempty"`
// Feature flags
Features map[string]bool `json:"features,omitempty"`
@ -83,7 +83,7 @@ type PasswordPolicy struct {
RequireLowercase bool `json:"require_lowercase"`
RequireNumbers bool `json:"require_numbers"`
RequireSymbols bool `json:"require_symbols"`
MaxAge time.Duration `json:"max_age,omitempty"`
MaxAge Duration `json:"max_age,omitempty"`
PreventReuse int `json:"prevent_reuse"` // Number of previous passwords to prevent reuse
}
@ -225,8 +225,8 @@ func (t *Tenant) GetAuthProvider() string {
// GetSessionTimeout returns the session timeout for the tenant
func (t *Tenant) GetSessionTimeout() time.Duration {
if t.Settings.SessionTimeout > 0 {
return t.Settings.SessionTimeout
if t.Settings.SessionTimeout.Duration > 0 {
return t.Settings.SessionTimeout.Duration
}
return 8 * time.Hour // default
}

View File

@ -48,8 +48,8 @@ func (r *ApplicationRepository) Create(ctx context.Context, app *domain.Applicat
pq.Array(typeStrings),
app.CallbackURL,
app.HMACKey,
app.TokenRenewalDuration.Nanoseconds(),
app.MaxTokenDuration.Nanoseconds(),
app.TokenRenewalDuration.Duration.Nanoseconds(),
app.MaxTokenDuration.Duration.Nanoseconds(),
string(app.Owner.Type),
app.Owner.Name,
app.Owner.Owner,
@ -118,8 +118,8 @@ func (r *ApplicationRepository) GetByID(ctx context.Context, appID string) (*dom
}
// Convert nanoseconds to duration
app.TokenRenewalDuration = time.Duration(tokenRenewalNanos)
app.MaxTokenDuration = time.Duration(maxTokenNanos)
app.TokenRenewalDuration = domain.Duration{Duration: time.Duration(tokenRenewalNanos)}
app.MaxTokenDuration = domain.Duration{Duration: time.Duration(maxTokenNanos)}
// Convert owner type
app.Owner.Type = domain.OwnerType(ownerType)
@ -180,8 +180,8 @@ func (r *ApplicationRepository) List(ctx context.Context, limit, offset int) ([]
}
// Convert nanoseconds to duration
app.TokenRenewalDuration = time.Duration(tokenRenewalNanos)
app.MaxTokenDuration = time.Duration(maxTokenNanos)
app.TokenRenewalDuration = domain.Duration{Duration: time.Duration(tokenRenewalNanos)}
app.MaxTokenDuration = domain.Duration{Duration: time.Duration(maxTokenNanos)}
// Convert owner type
app.Owner.Type = domain.OwnerType(ownerType)
@ -233,13 +233,13 @@ func (r *ApplicationRepository) Update(ctx context.Context, appID string, update
if updates.TokenRenewalDuration != nil {
setParts = append(setParts, fmt.Sprintf("token_renewal_duration = $%d", argIndex))
args = append(args, updates.TokenRenewalDuration.Nanoseconds())
args = append(args, updates.TokenRenewalDuration.Duration.Nanoseconds())
argIndex++
}
if updates.MaxTokenDuration != nil {
setParts = append(setParts, fmt.Sprintf("max_token_duration = $%d", argIndex))
args = append(args, updates.MaxTokenDuration.Nanoseconds())
args = append(args, updates.MaxTokenDuration.Duration.Nanoseconds())
argIndex++
}

View File

@ -375,7 +375,7 @@ func (s *sessionService) CreateOAuth2Session(ctx context.Context, userID, appID
expiresAt := time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second)
// Use application's max token duration if shorter
maxExpiration := time.Now().Add(app.MaxTokenDuration)
maxExpiration := time.Now().Add(app.MaxTokenDuration.Duration)
if expiresAt.After(maxExpiration) {
expiresAt = maxExpiration
}

7
kms-frontend/.env Normal file
View File

@ -0,0 +1,7 @@
# 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

23
kms-frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# 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*

213
kms-frontend/README.md Normal file
View File

@ -0,0 +1,213 @@
# 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

18774
kms-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
kms-frontend/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"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"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts 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.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!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.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

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

View File

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

262
kms-frontend/src/App.css Normal file
View File

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

View File

@ -0,0 +1,9 @@
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();
});

132
kms-frontend/src/App.tsx Normal file
View File

@ -0,0 +1,132 @@
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,
} 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 { 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: '/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="/users" element={<Users />} />
<Route path="/audit" element={<Audit />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Content>
</Layout>
</Layout>
);
};
const App: React.FC = () => {
return (
<ConfigProvider
theme={{
algorithm: theme.defaultAlgorithm,
}}
>
<AuthProvider>
<Router>
<AppContent />
</Router>
</AuthProvider>
</ConfigProvider>
);
};
export default App;

View File

@ -0,0 +1,487 @@
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_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_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: '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>
<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 Renewal Duration:</Text>
<div>{formatDuration(selectedApp.token_renewal_duration)}</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Max Token Duration:</Text>
<div>{formatDuration(selectedApp.max_token_duration)}</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedApp.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedApp.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedApp.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedApp.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedApp.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedApp.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Applications;

View File

@ -0,0 +1,519 @@
import React, { useState } 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';
dayjs.extend(relativeTime);
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const { Option } = Select;
interface AuditLogEntry {
id: string;
timestamp: string;
user_id: string;
action: string;
resource_type: string;
resource_id: string;
status: 'success' | 'failure' | 'warning';
ip_address: string;
user_agent: string;
details: Record<string, any>;
}
// Mock audit data for demonstration
const mockAuditData: AuditLogEntry[] = [
{
id: '1',
timestamp: dayjs().subtract(1, 'hour').toISOString(),
user_id: 'admin@example.com',
action: 'CREATE_APPLICATION',
resource_type: 'application',
resource_id: 'com.example.newapp',
status: 'success',
ip_address: '192.168.1.100',
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
details: {
app_link: 'https://newapp.example.com',
owner: 'Development Team'
}
},
{
id: '2',
timestamp: dayjs().subtract(2, 'hours').toISOString(),
user_id: 'user@example.com',
action: 'CREATE_TOKEN',
resource_type: 'token',
resource_id: 'token-abc123',
status: 'success',
ip_address: '192.168.1.101',
user_agent: 'curl/7.68.0',
details: {
app_id: 'com.example.app',
permissions: ['repo.read', 'repo.write']
}
},
{
id: '3',
timestamp: dayjs().subtract(3, 'hours').toISOString(),
user_id: 'admin@example.com',
action: 'DELETE_TOKEN',
resource_type: 'token',
resource_id: 'token-xyz789',
status: 'success',
ip_address: '192.168.1.100',
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
details: {
app_id: 'com.example.oldapp',
reason: 'Token compromised'
}
},
{
id: '4',
timestamp: dayjs().subtract(4, 'hours').toISOString(),
user_id: 'user@example.com',
action: 'VERIFY_TOKEN',
resource_type: 'token',
resource_id: 'token-def456',
status: 'failure',
ip_address: '192.168.1.102',
user_agent: 'PostmanRuntime/7.28.4',
details: {
app_id: 'com.example.app',
error: 'Token expired'
}
},
{
id: '5',
timestamp: dayjs().subtract(6, 'hours').toISOString(),
user_id: 'admin@example.com',
action: 'UPDATE_APPLICATION',
resource_type: 'application',
resource_id: 'com.example.app',
status: 'success',
ip_address: '192.168.1.100',
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
details: {
changes: {
callback_url: 'https://updated.example.com/callback'
}
}
},
];
const Audit: React.FC = () => {
const [auditData, setAuditData] = useState<AuditLogEntry[]>(mockAuditData);
const [filteredData, setFilteredData] = useState<AuditLogEntry[]>(mockAuditData);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({
dateRange: null as any,
action: '',
status: '',
user: '',
resourceType: '',
});
const applyFilters = () => {
let filtered = [...auditData];
if (filters.dateRange && filters.dateRange.length === 2) {
const [start, end] = filters.dateRange;
filtered = filtered.filter(entry => {
const entryDate = dayjs(entry.timestamp);
return entryDate.isAfter(start) && entryDate.isBefore(end);
});
}
if (filters.action) {
filtered = filtered.filter(entry => entry.action === filters.action);
}
if (filters.status) {
filtered = filtered.filter(entry => entry.status === filters.status);
}
if (filters.user) {
filtered = filtered.filter(entry =>
entry.user_id.toLowerCase().includes(filters.user.toLowerCase())
);
}
if (filters.resourceType) {
filtered = filtered.filter(entry => entry.resource_type === filters.resourceType);
}
setFilteredData(filtered);
};
const clearFilters = () => {
setFilters({
dateRange: null,
action: '',
status: '',
user: '',
resourceType: '',
});
setFilteredData(auditData);
};
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: AuditLogEntry, b: AuditLogEntry) =>
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
defaultSortOrder: 'descend' as const,
},
{
title: 'User',
dataIndex: 'user_id',
key: 'user_id',
render: (userId: string) => (
<div>
<UserOutlined style={{ marginRight: '8px' }} />
{userId}
</div>
),
},
{
title: 'Action',
dataIndex: 'action',
key: 'action',
render: (action: string) => (
<div>
{getActionIcon(action)}
<span style={{ marginLeft: '8px' }}>{action.replace(/_/g, ' ')}</span>
</div>
),
},
{
title: 'Resource',
key: 'resource',
render: (_: any, record: AuditLogEntry) => (
<div>
<div>
<Tag color="blue">{record.resource_type.toUpperCase()}</Tag>
</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{record.resource_id}
</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: 'ip_address',
key: 'ip_address',
render: (ip: string) => <Text code>{ip}</Text>,
},
];
const expandedRowRender = (record: AuditLogEntry) => (
<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.user_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="CREATE_APPLICATION">Create Application</Option>
<Option value="UPDATE_APPLICATION">Update Application</Option>
<Option value="DELETE_APPLICATION">Delete Application</Option>
<Option value="CREATE_TOKEN">Create Token</Option>
<Option value="DELETE_TOKEN">Delete Token</Option>
<Option value="VERIFY_TOKEN">Verify Token</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.action.replace(/_/g, ' ')}</Text>
<div>
<Text type="secondary">
{entry.user_id} {dayjs(entry.timestamp).fromNow()}
</Text>
</div>
<div>
<Tag>{entry.resource_type}</Tag>
<Text type="secondary" style={{ marginLeft: '8px' }}>
{entry.resource_id}
</Text>
</div>
</div>
</Timeline.Item>
))}
</Timeline>
</Card>
{/* Audit Log Table */}
<Card title="Audit Log Entries">
<Alert
message="Demo Data"
description="This audit log shows simulated data for demonstration purposes. In production, this would display real audit events from your KMS system."
type="info"
showIcon
style={{ marginBottom: '16px' }}
/>
<Table
columns={columns}
dataSource={filteredData}
rowKey="id"
loading={loading}
expandable={{
expandedRowRender,
expandRowByClick: true,
}}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} audit entries`,
}}
/>
</Card>
</Space>
</div>
);
};
export default Audit;

View File

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

View File

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

View File

@ -0,0 +1,652 @@
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 [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);
if (!values.app_id) {
message.error('Please select an application');
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);
setNewTokenResponse(response);
message.success('Token created successfully');
loadApplications();
} catch (error) {
console.error('Failed to create token:', error);
message.error('Failed to create token');
}
};
const handleVerifyToken = async (values: any) => {
try {
const verifyRequest: VerifyRequest = {
app_id: values.app_id,
type: 'static',
token: values.token,
permissions: values.permissions || [],
};
const response = await apiService.verifyToken(verifyRequest);
Modal.info({
title: 'Token Verification Result',
width: 600,
content: (
<div>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text strong>Status: </Text>
{response.valid ? (
<Tag color="green" icon={<CheckCircleOutlined />}>VALID</Tag>
) : (
<Tag color="red" icon={<ExclamationCircleOutlined />}>INVALID</Tag>
)}
</div>
{response.permissions && response.permissions.length > 0 && (
<div>
<Text strong>Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{response.permissions.map(permission => (
<Tag key={permission} color="blue">{permission}</Tag>
))}
</div>
</div>
)}
{response.permission_results && (
<div>
<Text strong>Permission Check Results:</Text>
<div style={{ marginTop: '8px' }}>
{Object.entries(response.permission_results).map(([permission, granted]) => (
<div key={permission} style={{ marginBottom: '4px' }}>
<Tag color={granted ? 'green' : 'red'}>
{permission}: {granted ? 'GRANTED' : 'DENIED'}
</Tag>
</div>
))}
</div>
</div>
)}
{response.error && (
<Alert
message="Error"
description={response.error}
type="error"
showIcon
/>
)}
</Space>
</div>
),
});
} catch (error) {
console.error('Failed to verify token:', error);
message.error('Failed to verify token');
}
};
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={() => setVerifyModalVisible(true)}
>
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()}
width={600}
>
<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"
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>
</Modal>
{/* Token Details Modal */}
<Modal
title="Token Details"
open={tokenDetailsVisible}
onCancel={() => setTokenDetailsVisible(false)}
footer={[
<Button key="close" onClick={() => setTokenDetailsVisible(false)}>
Close
</Button>,
]}
width={600}
>
{selectedToken && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Token Information">
<Row gutter={16}>
<Col span={12}>
<Text strong>Token ID:</Text>
<div>
<Text code>{selectedToken.id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedToken.id)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>Type:</Text>
<div>
<Tag color="blue">{selectedToken.type.toUpperCase()}</Tag>
</div>
</Col>
</Row>
</Card>
<Card title="Application">
<Row gutter={16}>
<Col span={12}>
<Text strong>App ID:</Text>
<div>
<Text code>{selectedToken.app?.app_id}</Text>
</div>
</Col>
<Col span={12}>
<Text strong>App Link:</Text>
<div>
<a href={selectedToken.app?.app_link} target="_blank" rel="noopener noreferrer">
{selectedToken.app?.app_link}
</a>
</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedToken.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedToken.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedToken.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedToken.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedToken.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedToken.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Tokens;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
kms-frontend/src/react-app-env.d.ts vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,206 @@
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_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_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;
type: string;
user_id?: string;
token: string;
permissions?: string[];
}
export interface VerifyResponse {
valid: 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;
}
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): Promise<any> {
const response = await this.api.post('/api/login', {
app_id: appId,
permissions,
redirect_uri: redirectUri,
});
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;
}
}
export const apiService = new ApiService();

View File

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

View File

@ -0,0 +1,26 @@
{
"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"
]
}

BIN
server Executable file

Binary file not shown.

View File

@ -231,8 +231,8 @@ func (suite *IntegrationTestSuite) TestApplicationCRUD() {
AppLink: "https://test-integration.example.com",
Type: []domain.ApplicationType{domain.ApplicationTypeStatic, domain.ApplicationTypeUser},
CallbackURL: "https://test-integration.example.com/callback",
TokenRenewalDuration: 7 * 24 * time.Hour, // 7 days
MaxTokenDuration: 30 * 24 * time.Hour, // 30 days
TokenRenewalDuration: domain.Duration{Duration: 7 * 24 * time.Hour}, // 7 days
MaxTokenDuration: domain.Duration{Duration: 30 * 24 * time.Hour}, // 30 days
Owner: domain.Owner{
Type: domain.OwnerTypeTeam,
Name: "Integration Test Team",
@ -333,8 +333,8 @@ func (suite *IntegrationTestSuite) TestStaticTokenWorkflow() {
AppLink: "https://test-token.example.com",
Type: []domain.ApplicationType{domain.ApplicationTypeStatic},
CallbackURL: "https://test-token.example.com/callback",
TokenRenewalDuration: 7 * 24 * time.Hour,
MaxTokenDuration: 30 * 24 * time.Hour,
TokenRenewalDuration: domain.Duration{Duration: 7 * 24 * time.Hour},
MaxTokenDuration: domain.Duration{Duration: 30 * 24 * time.Hour},
Owner: domain.Owner{
Type: domain.OwnerTypeIndividual,
Name: "Token Test User",
@ -438,8 +438,8 @@ func (suite *IntegrationTestSuite) TestUserTokenWorkflow() {
AppLink: "https://test-user.example.com",
Type: []domain.ApplicationType{domain.ApplicationTypeUser},
CallbackURL: "https://test-user.example.com/callback",
TokenRenewalDuration: 7 * 24 * time.Hour,
MaxTokenDuration: 30 * 24 * time.Hour,
TokenRenewalDuration: domain.Duration{Duration: 7 * 24 * time.Hour},
MaxTokenDuration: domain.Duration{Duration: 30 * 24 * time.Hour},
Owner: domain.Owner{
Type: domain.OwnerTypeTeam,
Name: "User Test Team",
@ -570,8 +570,8 @@ func (suite *IntegrationTestSuite) TestConcurrentRequests() {
AppLink: "https://test-concurrent.example.com",
Type: []domain.ApplicationType{domain.ApplicationTypeStatic},
CallbackURL: "https://test-concurrent.example.com/callback",
TokenRenewalDuration: 7 * 24 * time.Hour,
MaxTokenDuration: 30 * 24 * time.Hour,
TokenRenewalDuration: domain.Duration{Duration: 7 * 24 * time.Hour},
MaxTokenDuration: domain.Duration{Duration: 30 * 24 * time.Hour},
Owner: domain.Owner{
Type: domain.OwnerTypeTeam,
Name: "Concurrent Test Team",