Skip to content

Commit 814e31b

Browse files
author
Lasim
committed
feat(frontend): implement OAuth consent page and service integration
1 parent fc37c82 commit 814e31b

File tree

5 files changed

+437
-0
lines changed

5 files changed

+437
-0
lines changed

services/frontend/src/i18n/locales/en/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import credentialsMessages from './credentials'
1515
import mcpCatalogMessages from './mcp-catalog'
1616
import mcpCategoriesMessages from './mcp-categories'
1717
import mcpInstallationsMessages from './mcp-installations'
18+
import oauthMessages from './oauth'
1819

1920
export default {
2021
...commonMessages,
@@ -33,6 +34,7 @@ export default {
3334
...mcpCategoriesMessages,
3435
mcpCatalog: mcpCatalogMessages,
3536
mcpInstallations: mcpInstallationsMessages,
37+
oauth: oauthMessages,
3638
// If there are any top-level keys directly under 'en', they can be added here.
3739
// For example, if you had a global 'appName': 'My Application'
3840
// appName: 'DeployStack Application',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export default {
2+
consent: {
3+
title: 'Authorize Application',
4+
subtitle: 'Allow {clientName} to access your account?',
5+
signedInAs: 'Signed in as',
6+
permissionsTitle: 'Allowing will authorize {clientName} to:',
7+
buttons: {
8+
allow: 'Allow Access',
9+
deny: 'Deny Access',
10+
approving: 'Approving...',
11+
denying: 'Denying...'
12+
},
13+
security: {
14+
redirectNotice: 'This will redirect you back to the {clientName}.',
15+
revokeNotice: 'You can revoke this access at any time in your account settings.'
16+
},
17+
loading: {
18+
title: 'Loading consent details...',
19+
message: 'Please wait while we prepare the authorization request.'
20+
},
21+
errors: {
22+
title: 'Authorization Error',
23+
missingRequestId: 'Missing request ID parameter',
24+
requestNotFound: 'Authorization request not found or has expired',
25+
unauthorized: 'You must be logged in to authorize applications',
26+
invalidRequest: 'Invalid authorization request',
27+
networkError: 'Network error occurred. Please try again.',
28+
processingError: 'Failed to process authorization request',
29+
unknownError: 'An unknown error occurred',
30+
returnToDashboard: 'Return to Dashboard'
31+
},
32+
scopes: {
33+
'mcp:read': {
34+
name: 'MCP Server Access',
35+
description: 'Access your MCP server installations and configurations'
36+
},
37+
'account:read': {
38+
name: 'Account Information',
39+
description: 'Read your account information and settings'
40+
},
41+
'user:read': {
42+
name: 'User Profile',
43+
description: 'Read your user profile information'
44+
},
45+
'teams:read': {
46+
name: 'Team Access',
47+
description: 'Read your team memberships and team information'
48+
},
49+
'offline_access': {
50+
name: 'Offline Access',
51+
description: 'Maintain access when you\'re not actively using the application'
52+
}
53+
}
54+
}
55+
}

services/frontend/src/router/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ const routes = [
4949
component: () => import('../views/Logout.vue'),
5050
meta: { requiresSetup: true }, // Or false, depending on whether logout should be accessible if setup isn't complete
5151
},
52+
{
53+
path: '/oauth/consent',
54+
name: 'OAuthConsent',
55+
component: () => import('../views/oauth/ConsentPage.vue'),
56+
meta: {
57+
requiresSetup: true,
58+
title: 'Authorize Application'
59+
},
60+
},
5261
// Dashboard temporarily disabled - redirect users to MCP Server instead
5362
// {
5463
// path: '/dashboard',
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { getEnv } from '@/utils/env';
2+
3+
export interface ConsentDetails {
4+
success: boolean;
5+
request_id: string;
6+
client_id: string;
7+
client_name: string;
8+
user_email: string;
9+
scopes: Array<{
10+
name: string;
11+
description: string;
12+
}>;
13+
expires_at: string;
14+
}
15+
16+
export interface ConsentDecision {
17+
success: boolean;
18+
redirect_url?: string;
19+
error?: string;
20+
error_description?: string;
21+
}
22+
23+
export class OAuthService {
24+
private static getApiUrl(): string {
25+
const apiUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL');
26+
if (!apiUrl) {
27+
throw new Error('API URL not configured. Make sure VITE_DEPLOYSTACK_BACKEND_URL is set.');
28+
}
29+
return apiUrl;
30+
}
31+
32+
/**
33+
* Get consent details for an OAuth authorization request
34+
*/
35+
static async getConsentDetails(requestId: string): Promise<ConsentDetails> {
36+
try {
37+
const apiUrl = this.getApiUrl();
38+
const response = await fetch(`${apiUrl}/api/oauth2/consent/details?request_id=${encodeURIComponent(requestId)}`, {
39+
method: 'GET',
40+
headers: {
41+
'Content-Type': 'application/json',
42+
},
43+
credentials: 'include', // Important for sending session cookies
44+
});
45+
46+
if (!response.ok) {
47+
if (response.status === 401) {
48+
throw new Error('UNAUTHORIZED');
49+
}
50+
if (response.status === 404) {
51+
throw new Error('REQUEST_NOT_FOUND');
52+
}
53+
if (response.status === 400) {
54+
const errorData = await response.json().catch(() => ({}));
55+
throw new Error(errorData.error_description || 'INVALID_REQUEST');
56+
}
57+
58+
throw new Error(`Failed to get consent details: ${response.status}`);
59+
}
60+
61+
const data = await response.json();
62+
63+
if (!data.success) {
64+
throw new Error(data.error_description || 'Failed to get consent details');
65+
}
66+
67+
return data;
68+
} catch (error) {
69+
console.error('Error getting consent details:', error);
70+
throw error;
71+
}
72+
}
73+
74+
/**
75+
* Submit consent decision (approve or deny)
76+
*/
77+
static async submitConsentDecision(requestId: string, action: 'approve' | 'deny'): Promise<ConsentDecision> {
78+
try {
79+
const apiUrl = this.getApiUrl();
80+
const response = await fetch(`${apiUrl}/api/oauth2/consent`, {
81+
method: 'POST',
82+
headers: {
83+
'Content-Type': 'application/json',
84+
},
85+
credentials: 'include',
86+
body: JSON.stringify({
87+
request_id: requestId,
88+
action: action
89+
}),
90+
});
91+
92+
if (!response.ok) {
93+
if (response.status === 401) {
94+
throw new Error('UNAUTHORIZED');
95+
}
96+
if (response.status === 404) {
97+
throw new Error('REQUEST_NOT_FOUND');
98+
}
99+
if (response.status === 400) {
100+
const errorData = await response.json().catch(() => ({}));
101+
throw new Error(errorData.error_description || 'INVALID_REQUEST');
102+
}
103+
104+
throw new Error(`Failed to submit consent decision: ${response.status}`);
105+
}
106+
107+
const data = await response.json();
108+
109+
if (!data.success) {
110+
throw new Error(data.error_description || 'Failed to process consent decision');
111+
}
112+
113+
return data;
114+
} catch (error) {
115+
console.error('Error submitting consent decision:', error);
116+
throw error;
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)