@ratio-mcp/dev-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/developer.d.ts +13 -0
- package/dist/api/developer.d.ts.map +1 -0
- package/dist/api/developer.js +66 -0
- package/dist/api/developer.js.map +1 -0
- package/dist/api/oauth.d.ts +36 -0
- package/dist/api/oauth.d.ts.map +1 -0
- package/dist/api/oauth.js +77 -0
- package/dist/api/oauth.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +57 -0
- package/dist/server.js.map +1 -0
- package/dist/store/requirements-store.d.ts +8 -0
- package/dist/store/requirements-store.d.ts.map +1 -0
- package/dist/store/requirements-store.js +67 -0
- package/dist/store/requirements-store.js.map +1 -0
- package/dist/store/token-store.d.ts +12 -0
- package/dist/store/token-store.d.ts.map +1 -0
- package/dist/store/token-store.js +94 -0
- package/dist/store/token-store.js.map +1 -0
- package/dist/templates/backend/.env.example +20 -0
- package/dist/templates/backend/nest-cli.json +5 -0
- package/dist/templates/backend/package.json +26 -0
- package/dist/templates/backend/src/app.module.ts +10 -0
- package/dist/templates/backend/src/main.ts +12 -0
- package/dist/templates/backend/tsconfig.json +21 -0
- package/dist/templates/frontend/.env.example +6 -0
- package/dist/templates/frontend/index.html +13 -0
- package/dist/templates/frontend/package.json +24 -0
- package/dist/templates/frontend/src/App.tsx +32 -0
- package/dist/templates/frontend/src/components/Layout.tsx +53 -0
- package/dist/templates/frontend/src/context/AuthContext.tsx +59 -0
- package/dist/templates/frontend/src/index.css +48 -0
- package/dist/templates/frontend/src/main.tsx +16 -0
- package/dist/templates/frontend/src/pages/Callback.tsx +53 -0
- package/dist/templates/frontend/src/pages/Dashboard.tsx +24 -0
- package/dist/templates/frontend/src/pages/Login.tsx +42 -0
- package/dist/templates/frontend/src/services/api.ts +48 -0
- package/dist/templates/frontend/tsconfig.json +21 -0
- package/dist/templates/frontend/vite.config.ts +15 -0
- package/dist/tools/app-management.d.ts +3 -0
- package/dist/tools/app-management.d.ts.map +1 -0
- package/dist/tools/app-management.js +132 -0
- package/dist/tools/app-management.js.map +1 -0
- package/dist/tools/auth.d.ts +3 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/auth.js +164 -0
- package/dist/tools/auth.js.map +1 -0
- package/dist/tools/build.d.ts +3 -0
- package/dist/tools/build.d.ts.map +1 -0
- package/dist/tools/build.js +178 -0
- package/dist/tools/build.js.map +1 -0
- package/dist/tools/codegen.d.ts +3 -0
- package/dist/tools/codegen.d.ts.map +1 -0
- package/dist/tools/codegen.js +315 -0
- package/dist/tools/codegen.js.map +1 -0
- package/dist/tools/developer-auth.d.ts +3 -0
- package/dist/tools/developer-auth.d.ts.map +1 -0
- package/dist/tools/developer-auth.js +87 -0
- package/dist/tools/developer-auth.js.map +1 -0
- package/dist/tools/index.d.ts +16 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/lifecycle.d.ts +3 -0
- package/dist/tools/lifecycle.d.ts.map +1 -0
- package/dist/tools/lifecycle.js +95 -0
- package/dist/tools/lifecycle.js.map +1 -0
- package/dist/tools/requirements.d.ts +3 -0
- package/dist/tools/requirements.d.ts.map +1 -0
- package/dist/tools/requirements.js +169 -0
- package/dist/tools/requirements.js.map +1 -0
- package/dist/tools/scaffold.d.ts +3 -0
- package/dist/tools/scaffold.d.ts.map +1 -0
- package/dist/tools/scaffold.js +209 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/tools/submission.d.ts +3 -0
- package/dist/tools/submission.d.ts.map +1 -0
- package/dist/tools/submission.js +138 -0
- package/dist/tools/submission.js.map +1 -0
- package/dist/utils/errors.d.ts +34 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +81 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +29 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +74 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/zod-to-json.d.ts +7 -0
- package/dist/utils/zod-to-json.d.ts.map +1 -0
- package/dist/utils/zod-to-json.js +63 -0
- package/dist/utils/zod-to-json.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Ratio App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ratio-app-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0",
|
|
14
|
+
"react-router-dom": "^6.20.0",
|
|
15
|
+
"axios": "^1.6.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^18.2.0",
|
|
19
|
+
"@types/react-dom": "^18.2.0",
|
|
20
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
21
|
+
"typescript": "^5.3.0",
|
|
22
|
+
"vite": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
2
|
+
import { useAuth } from './context/AuthContext';
|
|
3
|
+
import Layout from './components/Layout';
|
|
4
|
+
import Dashboard from './pages/Dashboard';
|
|
5
|
+
import Login from './pages/Login';
|
|
6
|
+
import Callback from './pages/Callback';
|
|
7
|
+
|
|
8
|
+
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|
9
|
+
const { isAuthenticated } = useAuth();
|
|
10
|
+
return isAuthenticated ? <>{children}</> : <Navigate to="/login" />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function App() {
|
|
14
|
+
return (
|
|
15
|
+
<Routes>
|
|
16
|
+
<Route path="/login" element={<Login />} />
|
|
17
|
+
<Route path="/callback" element={<Callback />} />
|
|
18
|
+
<Route
|
|
19
|
+
path="/*"
|
|
20
|
+
element={
|
|
21
|
+
<PrivateRoute>
|
|
22
|
+
<Layout>
|
|
23
|
+
<Routes>
|
|
24
|
+
<Route path="/" element={<Dashboard />} />
|
|
25
|
+
</Routes>
|
|
26
|
+
</Layout>
|
|
27
|
+
</PrivateRoute>
|
|
28
|
+
}
|
|
29
|
+
/>
|
|
30
|
+
</Routes>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext';
|
|
4
|
+
|
|
5
|
+
export default function Layout({ children }: { children: ReactNode }) {
|
|
6
|
+
const { logout, merchantId } = useAuth();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ display: 'flex', minHeight: '100vh' }}>
|
|
10
|
+
{/* Sidebar */}
|
|
11
|
+
<aside
|
|
12
|
+
style={{
|
|
13
|
+
width: 240,
|
|
14
|
+
background: '#1a1a2e',
|
|
15
|
+
color: '#fff',
|
|
16
|
+
padding: '1.5rem 1rem',
|
|
17
|
+
display: 'flex',
|
|
18
|
+
flexDirection: 'column',
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<h2 style={{ fontSize: '1.2rem', marginBottom: '2rem' }}>Ratio App</h2>
|
|
22
|
+
<nav style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
23
|
+
<Link to="/" style={{ color: '#ccc', padding: '0.5rem', borderRadius: 6 }}>
|
|
24
|
+
Dashboard
|
|
25
|
+
</Link>
|
|
26
|
+
{/* Add more nav links here as you build pages */}
|
|
27
|
+
</nav>
|
|
28
|
+
<div style={{ borderTop: '1px solid #333', paddingTop: '1rem', fontSize: '0.85rem' }}>
|
|
29
|
+
<div style={{ color: '#888', marginBottom: '0.5rem' }}>
|
|
30
|
+
Merchant: {merchantId || '—'}
|
|
31
|
+
</div>
|
|
32
|
+
<button
|
|
33
|
+
onClick={logout}
|
|
34
|
+
style={{
|
|
35
|
+
width: '100%',
|
|
36
|
+
background: '#e74c3c',
|
|
37
|
+
border: 'none',
|
|
38
|
+
padding: '0.5rem',
|
|
39
|
+
borderRadius: 6,
|
|
40
|
+
color: '#fff',
|
|
41
|
+
cursor: 'pointer',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
Logout
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
</aside>
|
|
48
|
+
|
|
49
|
+
{/* Main Content */}
|
|
50
|
+
<main style={{ flex: 1, padding: '2rem' }}>{children}</main>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
2
|
+
import { api } from '../services/api';
|
|
3
|
+
|
|
4
|
+
interface AuthState {
|
|
5
|
+
accessToken: string | null;
|
|
6
|
+
merchantId: string | null;
|
|
7
|
+
isAuthenticated: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AuthContextType extends AuthState {
|
|
11
|
+
login: (token: string, merchantId: string) => void;
|
|
12
|
+
logout: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const AuthContext = createContext<AuthContextType | null>(null);
|
|
16
|
+
|
|
17
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
18
|
+
const [auth, setAuth] = useState<AuthState>({
|
|
19
|
+
accessToken: null,
|
|
20
|
+
merchantId: null,
|
|
21
|
+
isAuthenticated: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Check for stored token on mount
|
|
26
|
+
const token = sessionStorage.getItem('ratio_access_token');
|
|
27
|
+
const merchant = sessionStorage.getItem('ratio_merchant_id');
|
|
28
|
+
if (token) {
|
|
29
|
+
setAuth({ accessToken: token, merchantId: merchant, isAuthenticated: true });
|
|
30
|
+
api.setToken(token);
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const login = (token: string, merchantId: string) => {
|
|
35
|
+
sessionStorage.setItem('ratio_access_token', token);
|
|
36
|
+
sessionStorage.setItem('ratio_merchant_id', merchantId);
|
|
37
|
+
api.setToken(token);
|
|
38
|
+
setAuth({ accessToken: token, merchantId, isAuthenticated: true });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const logout = () => {
|
|
42
|
+
sessionStorage.removeItem('ratio_access_token');
|
|
43
|
+
sessionStorage.removeItem('ratio_merchant_id');
|
|
44
|
+
api.setToken(null);
|
|
45
|
+
setAuth({ accessToken: null, merchantId: null, isAuthenticated: false });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<AuthContext.Provider value={{ ...auth, login, logout }}>
|
|
50
|
+
{children}
|
|
51
|
+
</AuthContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useAuth() {
|
|
56
|
+
const context = useContext(AuthContext);
|
|
57
|
+
if (!context) throw new Error('useAuth must be used within AuthProvider');
|
|
58
|
+
return context;
|
|
59
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
color: #213547;
|
|
6
|
+
background-color: #f8f9fa;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
a {
|
|
20
|
+
color: #646cff;
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
a:hover {
|
|
25
|
+
color: #535bf2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
button {
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
border: 1px solid transparent;
|
|
31
|
+
padding: 0.6em 1.2em;
|
|
32
|
+
font-size: 1em;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
font-family: inherit;
|
|
35
|
+
background-color: #646cff;
|
|
36
|
+
color: white;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
transition: background-color 0.25s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button:hover {
|
|
42
|
+
background-color: #535bf2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
button:disabled {
|
|
46
|
+
opacity: 0.6;
|
|
47
|
+
cursor: not-allowed;
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import App from './App';
|
|
5
|
+
import { AuthProvider } from './context/AuthContext';
|
|
6
|
+
import './index.css';
|
|
7
|
+
|
|
8
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<BrowserRouter>
|
|
11
|
+
<AuthProvider>
|
|
12
|
+
<App />
|
|
13
|
+
</AuthProvider>
|
|
14
|
+
</BrowserRouter>
|
|
15
|
+
</React.StrictMode>,
|
|
16
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext';
|
|
4
|
+
|
|
5
|
+
export default function Callback() {
|
|
6
|
+
const [searchParams] = useSearchParams();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
const { login } = useAuth();
|
|
9
|
+
const [error, setError] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const code = searchParams.get('code');
|
|
13
|
+
if (!code) {
|
|
14
|
+
setError('No authorization code received.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Exchange the auth code for tokens via your backend
|
|
19
|
+
fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/auth/callback`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({ code }),
|
|
23
|
+
})
|
|
24
|
+
.then((res) => res.json())
|
|
25
|
+
.then((data) => {
|
|
26
|
+
if (data.access_token) {
|
|
27
|
+
login(data.access_token, data.merchant_id || '');
|
|
28
|
+
navigate('/');
|
|
29
|
+
} else {
|
|
30
|
+
setError(data.message || 'Token exchange failed.');
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch(() => setError('Failed to exchange authorization code.'));
|
|
34
|
+
}, [searchParams, login, navigate]);
|
|
35
|
+
|
|
36
|
+
if (error) {
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
39
|
+
<h2>Authentication Error</h2>
|
|
40
|
+
<p style={{ color: '#e74c3c' }}>{error}</p>
|
|
41
|
+
<button onClick={() => navigate('/login')} style={{ marginTop: '1rem' }}>
|
|
42
|
+
Back to Login
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
50
|
+
<p>Completing sign-in...</p>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useAuth } from '../context/AuthContext';
|
|
2
|
+
|
|
3
|
+
export default function Dashboard() {
|
|
4
|
+
const { merchantId } = useAuth();
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
<h1 style={{ marginBottom: '1rem' }}>Dashboard</h1>
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
background: '#fff',
|
|
12
|
+
borderRadius: 8,
|
|
13
|
+
padding: '1.5rem',
|
|
14
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<p>Welcome to your Ratio app. Merchant ID: <strong>{merchantId}</strong></p>
|
|
18
|
+
<p style={{ color: '#666', marginTop: '0.5rem' }}>
|
|
19
|
+
Start building your resource pages — orders, products, customers, etc.
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default function Login() {
|
|
2
|
+
const clientId = import.meta.env.VITE_RATIO_CLIENT_ID || '';
|
|
3
|
+
const redirectUri = import.meta.env.VITE_RATIO_REDIRECT_URI || `${window.location.origin}/callback`;
|
|
4
|
+
const dashboardUrl = import.meta.env.VITE_RATIO_DASHBOARD_URL || 'https://dev-developers.dev.gokwik.in';
|
|
5
|
+
|
|
6
|
+
const handleLogin = () => {
|
|
7
|
+
// Redirect to Ratio OAuth consent screen
|
|
8
|
+
const authUrl = `${dashboardUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`;
|
|
9
|
+
window.location.href = authUrl;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
style={{
|
|
15
|
+
display: 'flex',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
minHeight: '100vh',
|
|
19
|
+
background: '#f0f2f5',
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
style={{
|
|
24
|
+
background: '#fff',
|
|
25
|
+
padding: '3rem',
|
|
26
|
+
borderRadius: 12,
|
|
27
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
28
|
+
textAlign: 'center',
|
|
29
|
+
maxWidth: 400,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<h1 style={{ marginBottom: '0.5rem' }}>Ratio App</h1>
|
|
33
|
+
<p style={{ color: '#666', marginBottom: '2rem' }}>
|
|
34
|
+
Sign in with your Ratio merchant account to continue.
|
|
35
|
+
</p>
|
|
36
|
+
<button onClick={handleLogin} style={{ width: '100%', padding: '0.75rem' }}>
|
|
37
|
+
Sign in with Ratio
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
|
|
3
|
+
class ApiService {
|
|
4
|
+
private client: AxiosInstance;
|
|
5
|
+
private token: string | null = null;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
this.client = axios.create({
|
|
9
|
+
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Attach token to every request
|
|
14
|
+
this.client.interceptors.request.use((config) => {
|
|
15
|
+
if (this.token) {
|
|
16
|
+
config.headers.Authorization = `Bearer ${this.token}`;
|
|
17
|
+
}
|
|
18
|
+
return config;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setToken(token: string | null) {
|
|
23
|
+
this.token = token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Generic CRUD helpers — resource pages will use these
|
|
27
|
+
async get<T>(path: string, params?: Record<string, unknown>): Promise<T> {
|
|
28
|
+
const { data } = await this.client.get(path, { params });
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
33
|
+
const { data } = await this.client.post(path, body);
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
38
|
+
const { data } = await this.client.put(path, body);
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async delete<T>(path: string): Promise<T> {
|
|
43
|
+
const { data } = await this.client.delete(path);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const api = new ApiService();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"baseUrl": ".",
|
|
16
|
+
"paths": {
|
|
17
|
+
"@/*": ["./src/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
port: 5173,
|
|
8
|
+
proxy: {
|
|
9
|
+
'/api': {
|
|
10
|
+
target: 'http://localhost:3000',
|
|
11
|
+
changeOrigin: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-management.d.ts","sourceRoot":"","sources":["../../src/tools/app-management.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,eAAO,MAAM,kBAAkB,EAAE,cAAc,EA2H9C,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { developerApiRequest } from '../api/developer.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
export const appManagementTools = [
|
|
5
|
+
{
|
|
6
|
+
name: 'create_app',
|
|
7
|
+
description: 'Create a new application in the Ratio Developer Dashboard. Returns app_id, client_id, and client_secret.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
name: z.string().min(1).max(100).describe('Application name (required)'),
|
|
10
|
+
description: z.string().max(2000).optional().describe('Application description'),
|
|
11
|
+
category_ids: z.array(z.string()).min(1).describe('Category strings (e.g. ["analytics", "marketing"])'),
|
|
12
|
+
redirect_uri: z.string().optional().describe('OAuth callback URL'),
|
|
13
|
+
contact_email: z.string().optional().describe('Contact email'),
|
|
14
|
+
tags: z.array(z.string()).optional().describe('Application tags'),
|
|
15
|
+
icon: z.string().optional().describe('Icon URL'),
|
|
16
|
+
}),
|
|
17
|
+
handler: async (input) => {
|
|
18
|
+
const inp = input;
|
|
19
|
+
logger.info(`Creating application: ${inp.name}`);
|
|
20
|
+
const appData = { name: inp.name, categoryIds: inp.category_ids };
|
|
21
|
+
if (inp.description)
|
|
22
|
+
appData['description'] = inp.description;
|
|
23
|
+
if (inp.redirect_uri)
|
|
24
|
+
appData['redirectUri'] = inp.redirect_uri;
|
|
25
|
+
if (inp.contact_email)
|
|
26
|
+
appData['contactEmail'] = inp.contact_email;
|
|
27
|
+
if (inp.tags)
|
|
28
|
+
appData['tags'] = inp.tags;
|
|
29
|
+
if (inp.icon)
|
|
30
|
+
appData['icon'] = inp.icon;
|
|
31
|
+
const response = await developerApiRequest('POST', '/applications', appData);
|
|
32
|
+
const app = response.data;
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
message: 'Application created successfully',
|
|
36
|
+
application: {
|
|
37
|
+
id: app.id,
|
|
38
|
+
name: app.name,
|
|
39
|
+
slug: app.slug,
|
|
40
|
+
client_id: app.clientId,
|
|
41
|
+
client_secret: app.clientSecret,
|
|
42
|
+
status: app.status,
|
|
43
|
+
},
|
|
44
|
+
next_steps: [
|
|
45
|
+
'1. Save client_id and client_secret securely',
|
|
46
|
+
'2. Call define_app_requirements with scopes',
|
|
47
|
+
'3. Call scaffold_frontend to create the frontend project',
|
|
48
|
+
'4. Build, zip, upload, and submit for review',
|
|
49
|
+
'5. After admin approval: OAuth → scaffold_backend → generate_api_routes',
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'get_app',
|
|
56
|
+
description: 'Get details of an existing application from the Ratio Developer Dashboard.',
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
app_id: z.string().min(1).describe('Application ID to retrieve'),
|
|
59
|
+
}),
|
|
60
|
+
handler: async (input) => {
|
|
61
|
+
const { app_id } = input;
|
|
62
|
+
const response = await developerApiRequest('GET', `/applications/${app_id}`);
|
|
63
|
+
return { success: true, application: response };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'update_app',
|
|
68
|
+
description: 'Update an existing application. Only provided fields will be updated.',
|
|
69
|
+
inputSchema: z.object({
|
|
70
|
+
app_id: z.string().min(1).describe('Application ID to update'),
|
|
71
|
+
name: z.string().optional().describe('New name'),
|
|
72
|
+
description: z.string().optional().describe('New description'),
|
|
73
|
+
category_ids: z.array(z.string()).optional().describe('New categories'),
|
|
74
|
+
redirect_uri: z.string().optional().describe('New redirect URI'),
|
|
75
|
+
contact_email: z.string().optional().describe('New contact email'),
|
|
76
|
+
tags: z.array(z.string()).optional().describe('New tags'),
|
|
77
|
+
icon: z.string().optional().describe('New icon URL'),
|
|
78
|
+
}),
|
|
79
|
+
handler: async (input) => {
|
|
80
|
+
const inp = input;
|
|
81
|
+
const updateData = {};
|
|
82
|
+
if (inp.name !== undefined)
|
|
83
|
+
updateData['name'] = inp.name;
|
|
84
|
+
if (inp.description !== undefined)
|
|
85
|
+
updateData['description'] = inp.description;
|
|
86
|
+
if (inp.category_ids !== undefined)
|
|
87
|
+
updateData['categoryIds'] = inp.category_ids;
|
|
88
|
+
if (inp.redirect_uri !== undefined)
|
|
89
|
+
updateData['redirectUri'] = inp.redirect_uri;
|
|
90
|
+
if (inp.contact_email !== undefined)
|
|
91
|
+
updateData['contactEmail'] = inp.contact_email;
|
|
92
|
+
if (inp.tags !== undefined)
|
|
93
|
+
updateData['tags'] = inp.tags;
|
|
94
|
+
if (inp.icon !== undefined)
|
|
95
|
+
updateData['icon'] = inp.icon;
|
|
96
|
+
const response = await developerApiRequest('PATCH', `/applications/${inp.app_id}`, updateData);
|
|
97
|
+
return { success: true, message: 'Application updated', application: response };
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'delete_app',
|
|
102
|
+
description: 'Delete an application. This action cannot be undone.',
|
|
103
|
+
inputSchema: z.object({
|
|
104
|
+
app_id: z.string().min(1).describe('Application ID to delete'),
|
|
105
|
+
confirm: z.boolean().describe('Set to true to confirm deletion'),
|
|
106
|
+
}),
|
|
107
|
+
handler: async (input) => {
|
|
108
|
+
const { app_id, confirm } = input;
|
|
109
|
+
if (!confirm)
|
|
110
|
+
return { success: false, message: 'Deletion not confirmed. Set confirm: true.' };
|
|
111
|
+
await developerApiRequest('DELETE', `/applications/${app_id}`);
|
|
112
|
+
return { success: true, message: `Application ${app_id} deleted` };
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'list_apps',
|
|
117
|
+
description: 'List all applications created by the developer.',
|
|
118
|
+
inputSchema: z.object({}),
|
|
119
|
+
handler: async () => {
|
|
120
|
+
const response = await developerApiRequest('GET', '/applications');
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
total: response.data.total,
|
|
124
|
+
applications: response.data.applications.map((app) => ({
|
|
125
|
+
id: app.id, name: app.name, slug: app.slug,
|
|
126
|
+
client_id: app.clientId, status: app.status, description: app.description,
|
|
127
|
+
})),
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
//# sourceMappingURL=app-management.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-management.js","sourceRoot":"","sources":["../../src/tools/app-management.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/tools/auth.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,eAAO,MAAM,SAAS,EAAE,cAAc,EAgLrC,CAAC"}
|