modification in git-service oct 14
This commit is contained in:
parent
82940e41a2
commit
ab8b8942e8
42
.env.example
42
.env.example
@ -1,24 +1,24 @@
|
|||||||
# Database Configuration
|
# =====================================
|
||||||
POSTGRES_USER=pipeline_admin
|
# VCS OAuth Configuration
|
||||||
POSTGRES_PASSWORD=your_secure_password
|
# =====================================
|
||||||
POSTGRES_DB=dev_pipeline
|
|
||||||
|
|
||||||
# Redis Configuration
|
# GitLab OAuth Configuration
|
||||||
REDIS_PASSWORD=your_redis_password
|
GITLAB_CLIENT_ID=your_gitlab_client_id_here
|
||||||
|
GITLAB_CLIENT_SECRET=your_gitlab_client_secret_here
|
||||||
|
GITLAB_BASE_URL=https://gitlab.com
|
||||||
|
GITLAB_REDIRECT_URI=http://localhost:8000/api/vcs/gitlab/auth/callback
|
||||||
|
GITLAB_WEBHOOK_SECRET=your_gitlab_webhook_secret_here
|
||||||
|
|
||||||
# MongoDB Configuration
|
# Bitbucket OAuth Configuration
|
||||||
MONGO_INITDB_ROOT_USERNAME=pipeline_admin
|
BITBUCKET_CLIENT_ID=your_bitbucket_client_id_here
|
||||||
MONGO_INITDB_ROOT_PASSWORD=your_mongo_password
|
BITBUCKET_CLIENT_SECRET=your_bitbucket_client_secret_here
|
||||||
|
BITBUCKET_REDIRECT_URI=http://localhost:8000/api/vcs/bitbucket/auth/callback
|
||||||
|
BITBUCKET_OAUTH_SCOPES=repository account
|
||||||
|
BITBUCKET_WEBHOOK_SECRET=your_bitbucket_webhook_secret_here
|
||||||
|
|
||||||
# RabbitMQ Configuration
|
# Gitea OAuth Configuration
|
||||||
RABBITMQ_DEFAULT_USER=pipeline_admin
|
GITEA_CLIENT_ID=your_gitea_client_id_here
|
||||||
RABBITMQ_DEFAULT_PASS=your_rabbit_password
|
GITEA_CLIENT_SECRET=your_gitea_client_secret_here
|
||||||
|
GITEA_BASE_URL=https://gitea.com
|
||||||
# API Keys
|
GITEA_REDIRECT_URI=http://localhost:8000/api/vcs/gitea/auth/callback
|
||||||
CLAUDE_API_KEY=your_claude_api_key_here
|
GITEA_WEBHOOK_SECRET=your_gitea_webhook_secret_here
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
|
||||||
CLOUDTOPIAA_API_KEY=your_cloudtopiaa_api_key_here
|
|
||||||
CLOUDTOPIAA_API_URL=https://api.cloudtopiaa.com
|
|
||||||
|
|
||||||
# JWT Configuration
|
|
||||||
JWT_SECRET=your_jwt_secret_here
|
|
||||||
|
|||||||
@ -636,6 +636,24 @@ services:
|
|||||||
- GITHUB_CLIENT_SECRET=8bf82a29154fdccb837bc150539a2226d00b5da5
|
- GITHUB_CLIENT_SECRET=8bf82a29154fdccb837bc150539a2226d00b5da5
|
||||||
- GITHUB_REDIRECT_URI=http://localhost:8000/api/github/auth/github/callback
|
- GITHUB_REDIRECT_URI=http://localhost:8000/api/github/auth/github/callback
|
||||||
- GITHUB_WEBHOOK_SECRET=mywebhooksecret2025
|
- GITHUB_WEBHOOK_SECRET=mywebhooksecret2025
|
||||||
|
# GitLab OAuth Configuration
|
||||||
|
- GITLAB_CLIENT_ID=f05b0ab3ff6d5d26e1350ccf42d6394e085e343251faa07176991355112d4348
|
||||||
|
- GITLAB_CLIENT_SECRET=gloas-a2c11ed9bd84201d7773f264cad6e86a116355d80c24a68000cebfc92ebe2411
|
||||||
|
- GITLAB_BASE_URL=https://gitlab.com
|
||||||
|
- GITLAB_REDIRECT_URI=http://localhost:8000/api/vcs/gitlab/auth/callback
|
||||||
|
- GITLAB_WEBHOOK_SECRET=mywebhooksecret2025
|
||||||
|
# Bitbucket OAuth Configuration
|
||||||
|
- BITBUCKET_CLIENT_ID=ZhdD8bbfugEUS4aL7v
|
||||||
|
- BITBUCKET_CLIENT_SECRET=K3dY3PFQRJUGYwBtERpHMswrRHbmK8qw
|
||||||
|
- BITBUCKET_REDIRECT_URI=http://localhost:8000/api/vcs/bitbucket/auth/callback
|
||||||
|
- BITBUCKET_OAUTH_SCOPES=repository account
|
||||||
|
- BITBUCKET_WEBHOOK_SECRET=mywebhooksecret2025
|
||||||
|
# Gitea OAuth Configuration
|
||||||
|
- GITEA_CLIENT_ID=67c9cdb9-c15a-4c02-9c85-31cfd1c62ef2
|
||||||
|
- GITEA_CLIENT_SECRET=gto_vmom6izq6ysaa7wmiq24whz5oe3zki2cmhljszgbg5yourlxfrua
|
||||||
|
- GITEA_BASE_URL=https://gitea.com
|
||||||
|
- GITEA_REDIRECT_URI=http://localhost:8000/api/vcs/gitea/auth/callback
|
||||||
|
- GITEA_WEBHOOK_SECRET=mywebhooksecret2025
|
||||||
- PUBLIC_BASE_URL=https://7922be5648be.ngrok-free.app
|
- PUBLIC_BASE_URL=https://7922be5648be.ngrok-free.app
|
||||||
- ATTACHED_REPOS_DIR=/app/git-repos
|
- ATTACHED_REPOS_DIR=/app/git-repos
|
||||||
- DIFF_STORAGE_DIR=/app/git-diff
|
- DIFF_STORAGE_DIR=/app/git-diff
|
||||||
|
|||||||
331
services/git-integration/MULTI_VCS_IMPLEMENTATION_GUIDE.md
Normal file
331
services/git-integration/MULTI_VCS_IMPLEMENTATION_GUIDE.md
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
# Multi-VCS Implementation Guide
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
### ✅ Completed Components
|
||||||
|
|
||||||
|
1. **OAuth Services (GitLab, Bitbucket, Gitea)**
|
||||||
|
- All OAuth services updated with user_id support
|
||||||
|
- Following GitHub pattern exactly
|
||||||
|
- Methods: `getAuthUrl(state, userId)`, `storeToken(accessToken, user, userId)`, `getTokenForUser(userId)`
|
||||||
|
|
||||||
|
2. **Database Migrations**
|
||||||
|
- Migration `022_multi_vcs_provider_support.sql` created
|
||||||
|
- Token tables: `gitlab_user_tokens`, `bitbucket_user_tokens`, `gitea_user_tokens`
|
||||||
|
- Webhook tables: `gitlab_webhooks`, `bitbucket_webhooks`, `gitea_webhooks`
|
||||||
|
- Provider name column added to `all_repositories`
|
||||||
|
|
||||||
|
3. **Provider Adapters**
|
||||||
|
- Updated `checkRepositoryAccess(owner, repo, userId)` in all adapters
|
||||||
|
- GitLab, Bitbucket, Gitea adapters support user-specific tokens
|
||||||
|
|
||||||
|
### 🔄 Pending Implementation
|
||||||
|
|
||||||
|
#### 1. VCS Routes OAuth Callback Update
|
||||||
|
|
||||||
|
**File:** `src/routes/vcs.routes.js`
|
||||||
|
|
||||||
|
**Current Issue:** OAuth callback doesn't extract and use user_id properly
|
||||||
|
|
||||||
|
**Required Changes:**
|
||||||
|
```javascript
|
||||||
|
// In router.get('/:provider/auth/callback')
|
||||||
|
// Extract user_id from multiple sources (like GitHub)
|
||||||
|
let user_id =
|
||||||
|
req.query.user_id ||
|
||||||
|
(req.body && req.body.user_id) ||
|
||||||
|
req.headers['x-user-id'] ||
|
||||||
|
(req.cookies && (req.cookies.user_id || req.cookies.uid)) ||
|
||||||
|
(req.session && req.session.user && (req.session.user.id || req.session.user.userId)) ||
|
||||||
|
(req.user && (req.user.id || req.user.userId));
|
||||||
|
|
||||||
|
// Also extract from state
|
||||||
|
if (!user_id && typeof state === 'string' && state.includes('|uid=')) {
|
||||||
|
try { user_id = state.split('|uid=')[1].split('|')[0]; } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store token with user_id
|
||||||
|
const tokenRecord = await oauth.storeToken(accessToken, user, user_id);
|
||||||
|
|
||||||
|
// Redirect with provider info
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || 'https://dashboard.codenuk.com';
|
||||||
|
const redirectUrl = `${frontendUrl}/project-builder?${providerKey}_connected=1&user=${encodeURIComponent(user.username || user.login)}`;
|
||||||
|
res.redirect(302, redirectUrl);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Frontend Integration
|
||||||
|
|
||||||
|
**File:** `fronend/codenuk_frontend_mine/src/lib/api/github.ts`
|
||||||
|
|
||||||
|
**Add Provider Detection Function:**
|
||||||
|
```typescript
|
||||||
|
export function detectProvider(repoUrl: string): string {
|
||||||
|
if (repoUrl.includes('github.com')) return 'github';
|
||||||
|
if (repoUrl.includes('gitlab.com') || repoUrl.includes('gitlab')) return 'gitlab';
|
||||||
|
if (repoUrl.includes('bitbucket.org')) return 'bitbucket';
|
||||||
|
if (repoUrl.includes('gitea')) return 'gitea';
|
||||||
|
throw new Error('Unsupported repository provider');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update `attachRepository` Function:**
|
||||||
|
```typescript
|
||||||
|
export async function attachRepository(payload: AttachRepositoryPayload): Promise<AttachRepositoryResponse> {
|
||||||
|
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null;
|
||||||
|
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null;
|
||||||
|
|
||||||
|
// Detect provider from URL
|
||||||
|
const provider = detectProvider(payload.repository_url);
|
||||||
|
|
||||||
|
const url = userId
|
||||||
|
? `/api/vcs/${provider}/attach-repository?user_id=${encodeURIComponent(userId)}`
|
||||||
|
: `/api/vcs/${provider}/attach-repository`;
|
||||||
|
|
||||||
|
const response = await authApiClient.post(url, {
|
||||||
|
...payload,
|
||||||
|
user_id: userId
|
||||||
|
}, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 60000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle auth required response
|
||||||
|
if (response.status === 401 && response.data.requires_auth) {
|
||||||
|
window.location.href = response.data.auth_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add Multi-Provider OAuth Functions:**
|
||||||
|
```typescript
|
||||||
|
export async function connectProvider(provider: string, repoUrl?: string, branch?: string): Promise<void> {
|
||||||
|
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null;
|
||||||
|
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
alert('Please sign in first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateBase = Math.random().toString(36).substring(7);
|
||||||
|
let state = stateBase;
|
||||||
|
|
||||||
|
if (repoUrl) {
|
||||||
|
const encodedRepoUrl = encodeURIComponent(repoUrl);
|
||||||
|
const encodedBranch = encodeURIComponent(branch || 'main');
|
||||||
|
state = `${stateBase}|uid=${userId}|repo=${encodedRepoUrl}|branch=${encodedBranch}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
||||||
|
repository_url: repoUrl,
|
||||||
|
branch_name: branch || 'main',
|
||||||
|
provider: provider
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to store pending attach:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authApiClient.get(
|
||||||
|
`/api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const authUrl = response.data?.auth_url;
|
||||||
|
if (authUrl) {
|
||||||
|
window.location.href = authUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Frontend OAuth Callback Handling
|
||||||
|
|
||||||
|
**File:** `fronend/codenuk_frontend_mine/src/app/project-builder/page.tsx`
|
||||||
|
|
||||||
|
**Update to Handle All Providers:**
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading || !user) return;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Check for any provider's OAuth success
|
||||||
|
const providers = ['github', 'gitlab', 'bitbucket', 'gitea'];
|
||||||
|
const connectedProvider = providers.find(provider =>
|
||||||
|
params.get(`${provider}_connected`) === '1'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (connectedProvider) {
|
||||||
|
const providerUser = params.get('user');
|
||||||
|
const repoAttached = params.get('repo_attached') === '1';
|
||||||
|
const repositoryId = params.get('repository_id');
|
||||||
|
const syncStatus = params.get('sync_status');
|
||||||
|
|
||||||
|
// Clear pending git attach
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem('pending_git_attach');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to clear pending attach:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repoAttached && repositoryId) {
|
||||||
|
alert(`${connectedProvider.toUpperCase()} repository attached successfully!\n\nUser: ${providerUser}\nRepository ID: ${repositoryId}\nSync Status: ${syncStatus}`);
|
||||||
|
} else {
|
||||||
|
alert(`${connectedProvider.toUpperCase()} account connected successfully!\n\nUser: ${providerUser}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up URL parameters
|
||||||
|
router.replace('/project-builder');
|
||||||
|
}
|
||||||
|
}, [isLoading, user, searchParams, router]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Frontend UI Component Updates
|
||||||
|
|
||||||
|
**File:** `fronend/codenuk_frontend_mine/src/components/main-dashboard.tsx`
|
||||||
|
|
||||||
|
**Update Git URL Input Handler:**
|
||||||
|
```typescript
|
||||||
|
const handleCreateFromGit = async () => {
|
||||||
|
try {
|
||||||
|
if (!gitUrl.trim()) {
|
||||||
|
alert('Please enter a repository URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect provider from URL
|
||||||
|
const provider = detectProvider(gitUrl.trim());
|
||||||
|
console.log('Detected provider:', provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await attachRepository({
|
||||||
|
template_id: selectedTemplate?.id,
|
||||||
|
repository_url: gitUrl.trim(),
|
||||||
|
branch_name: gitBranch?.trim() || 'main',
|
||||||
|
git_provider: provider
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`Repository attached successfully!`);
|
||||||
|
// Handle success
|
||||||
|
}
|
||||||
|
} catch (attachErr) {
|
||||||
|
const err: any = attachErr;
|
||||||
|
const status = err?.response?.status;
|
||||||
|
const data = err?.response?.data;
|
||||||
|
|
||||||
|
// If backend signals auth required, redirect to OAuth
|
||||||
|
if (status === 401 && data?.requires_auth) {
|
||||||
|
const authUrl: string = data?.auth_url;
|
||||||
|
if (!authUrl) {
|
||||||
|
alert('Authentication URL is missing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist pending repo
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
||||||
|
repository_url: gitUrl.trim(),
|
||||||
|
branch_name: gitBranch?.trim() || 'main',
|
||||||
|
provider: provider
|
||||||
|
}));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
console.log(`Redirecting to ${provider} OAuth:`, authUrl);
|
||||||
|
window.location.replace(authUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(data?.message || `Failed to attach repository.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing from Git:', error);
|
||||||
|
alert(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables Required
|
||||||
|
|
||||||
|
Add these to your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GitLab OAuth
|
||||||
|
GITLAB_CLIENT_ID=your_gitlab_client_id
|
||||||
|
GITLAB_CLIENT_SECRET=your_gitlab_client_secret
|
||||||
|
GITLAB_BASE_URL=https://gitlab.com
|
||||||
|
GITLAB_REDIRECT_URI=http://localhost:8000/api/vcs/gitlab/auth/callback
|
||||||
|
GITLAB_WEBHOOK_SECRET=your_gitlab_webhook_secret
|
||||||
|
|
||||||
|
# Bitbucket OAuth
|
||||||
|
BITBUCKET_CLIENT_ID=your_bitbucket_client_id
|
||||||
|
BITBUCKET_CLIENT_SECRET=your_bitbucket_client_secret
|
||||||
|
BITBUCKET_REDIRECT_URI=http://localhost:8000/api/vcs/bitbucket/auth/callback
|
||||||
|
BITBUCKET_OAUTH_SCOPES=repository account webhook
|
||||||
|
|
||||||
|
# Gitea OAuth
|
||||||
|
GITEA_CLIENT_ID=your_gitea_client_id
|
||||||
|
GITEA_CLIENT_SECRET=your_gitea_client_secret
|
||||||
|
GITEA_BASE_URL=https://gitea.com
|
||||||
|
GITEA_REDIRECT_URI=http://localhost:8000/api/vcs/gitea/auth/callback
|
||||||
|
|
||||||
|
# Frontend URL
|
||||||
|
FRONTEND_URL=http://localhost:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Implementation
|
||||||
|
|
||||||
|
### 1. Run Database Migration
|
||||||
|
```bash
|
||||||
|
cd services/git-integration
|
||||||
|
npm run migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test GitLab OAuth
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/vcs/gitlab/auth/start?user_id=YOUR_USER_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Bitbucket OAuth
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/vcs/bitbucket/auth/start?user_id=YOUR_USER_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Gitea OAuth
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/vcs/gitea/auth/start?user_id=YOUR_USER_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Test Repository Attachment
|
||||||
|
```bash
|
||||||
|
# GitLab
|
||||||
|
curl -X POST "http://localhost:8000/api/vcs/gitlab/attach-repository" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-user-id: YOUR_USER_ID" \
|
||||||
|
-d '{"template_id": "template-id", "repository_url": "https://gitlab.com/owner/repo", "branch_name": "main"}'
|
||||||
|
|
||||||
|
# Bitbucket
|
||||||
|
curl -X POST "http://localhost:8000/api/vcs/bitbucket/attach-repository" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-user-id: YOUR_USER_ID" \
|
||||||
|
-d '{"template_id": "template-id", "repository_url": "https://bitbucket.org/owner/repo", "branch_name": "main"}'
|
||||||
|
|
||||||
|
# Gitea
|
||||||
|
curl -X POST "http://localhost:8000/api/vcs/gitea/attach-repository" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-user-id: YOUR_USER_ID" \
|
||||||
|
-d '{"template_id": "template-id", "repository_url": "https://gitea.com/owner/repo", "branch_name": "main"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The implementation follows the GitHub pattern exactly:
|
||||||
|
- OAuth flow with user_id linkage
|
||||||
|
- Token storage in provider-specific tables
|
||||||
|
- User-specific token retrieval
|
||||||
|
- Repository access checks with user context
|
||||||
|
- Webhook support for all providers
|
||||||
|
- AI streaming (already provider-agnostic)
|
||||||
|
|
||||||
|
All providers now work identically to GitHub from the user's perspective!
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
-- Migration 022: Multi-VCS Provider Support - User ID Linkage
|
||||||
|
-- This migration adds user_id and is_primary columns to existing provider token tables
|
||||||
|
-- and adds missing indexes for multi-account support
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Add user_id and is_primary columns to existing token tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- GitLab User Tokens - Add user_id and is_primary columns
|
||||||
|
ALTER TABLE gitlab_user_tokens
|
||||||
|
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE gitlab_user_tokens
|
||||||
|
ADD COLUMN IF NOT EXISTS is_primary BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Bitbucket User Tokens - Add user_id and is_primary columns
|
||||||
|
ALTER TABLE bitbucket_user_tokens
|
||||||
|
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE bitbucket_user_tokens
|
||||||
|
ADD COLUMN IF NOT EXISTS is_primary BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Gitea User Tokens - Add user_id and is_primary columns
|
||||||
|
ALTER TABLE gitea_user_tokens
|
||||||
|
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE gitea_user_tokens
|
||||||
|
ADD COLUMN IF NOT EXISTS is_primary BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Add indexes for multi-account support
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- GitLab User Tokens indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gitlab_user_tokens_user_id ON gitlab_user_tokens(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gitlab_user_tokens_user_gitlab ON gitlab_user_tokens(user_id, gitlab_username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gitlab_user_tokens_primary ON gitlab_user_tokens(user_id, is_primary);
|
||||||
|
|
||||||
|
-- Bitbucket User Tokens indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bitbucket_user_tokens_user_id ON bitbucket_user_tokens(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bitbucket_user_tokens_user_bitbucket ON bitbucket_user_tokens(user_id, bitbucket_username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bitbucket_user_tokens_primary ON bitbucket_user_tokens(user_id, is_primary);
|
||||||
|
|
||||||
|
-- Gitea User Tokens indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gitea_user_tokens_user_id ON gitea_user_tokens(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gitea_user_tokens_user_gitea ON gitea_user_tokens(user_id, gitea_username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gitea_user_tokens_primary ON gitea_user_tokens(user_id, is_primary);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Add unique constraints for multi-account support
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Create unique constraint: one token per GitLab account per user
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_gitlab_user_tokens_unique_user_gitlab
|
||||||
|
ON gitlab_user_tokens(user_id, gitlab_username)
|
||||||
|
WHERE user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Create unique constraint: one token per Bitbucket account per user
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_bitbucket_user_tokens_unique_user_bitbucket
|
||||||
|
ON bitbucket_user_tokens(user_id, bitbucket_username)
|
||||||
|
WHERE user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Create unique constraint: one token per Gitea account per user
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_gitea_user_tokens_unique_user_gitea
|
||||||
|
ON gitea_user_tokens(user_id, gitea_username)
|
||||||
|
WHERE user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Add comments for documentation
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
COMMENT ON COLUMN gitlab_user_tokens.user_id IS 'User ID linking this GitLab token to a specific user';
|
||||||
|
COMMENT ON COLUMN gitlab_user_tokens.is_primary IS 'Whether this is the primary GitLab account for the user';
|
||||||
|
COMMENT ON COLUMN bitbucket_user_tokens.user_id IS 'User ID linking this Bitbucket token to a specific user';
|
||||||
|
COMMENT ON COLUMN bitbucket_user_tokens.is_primary IS 'Whether this is the primary Bitbucket account for the user';
|
||||||
|
COMMENT ON COLUMN gitea_user_tokens.user_id IS 'User ID linking this Gitea token to a specific user';
|
||||||
|
COMMENT ON COLUMN gitea_user_tokens.is_primary IS 'Whether this is the primary Gitea account for the user';
|
||||||
@ -6,6 +6,7 @@ const database = require('../config/database');
|
|||||||
const FileStorageService = require('../services/file-storage.service');
|
const FileStorageService = require('../services/file-storage.service');
|
||||||
|
|
||||||
const fileStorageService = new FileStorageService();
|
const fileStorageService = new FileStorageService();
|
||||||
|
const GitHubOAuthService = require('../services/github-oauth');
|
||||||
const GitLabOAuthService = require('../services/gitlab-oauth');
|
const GitLabOAuthService = require('../services/gitlab-oauth');
|
||||||
const BitbucketOAuthService = require('../services/bitbucket-oauth');
|
const BitbucketOAuthService = require('../services/bitbucket-oauth');
|
||||||
const GiteaOAuthService = require('../services/gitea-oauth');
|
const GiteaOAuthService = require('../services/gitea-oauth');
|
||||||
@ -19,6 +20,7 @@ function getProvider(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getOAuthService(providerKey) {
|
function getOAuthService(providerKey) {
|
||||||
|
if (providerKey === 'github') return new GitHubOAuthService();
|
||||||
if (providerKey === 'gitlab') return new GitLabOAuthService();
|
if (providerKey === 'gitlab') return new GitLabOAuthService();
|
||||||
if (providerKey === 'bitbucket') return new BitbucketOAuthService();
|
if (providerKey === 'bitbucket') return new BitbucketOAuthService();
|
||||||
if (providerKey === 'gitea') return new GiteaOAuthService();
|
if (providerKey === 'gitea') return new GiteaOAuthService();
|
||||||
@ -45,17 +47,13 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
|||||||
const { template_id, repository_url, branch_name } = req.body;
|
const { template_id, repository_url, branch_name } = req.body;
|
||||||
const userId = req.headers['x-user-id'] || req.query.user_id || req.body.user_id || (req.user && (req.user.id || req.user.userId));
|
const userId = req.headers['x-user-id'] || req.query.user_id || req.body.user_id || (req.user && (req.user.id || req.user.userId));
|
||||||
|
|
||||||
if (!template_id || !repository_url) {
|
// Validate input - only repository_url is required (like GitHub)
|
||||||
return res.status(400).json({ success: false, message: 'Template ID and repository URL are required' });
|
if (!repository_url) {
|
||||||
}
|
return res.status(400).json({ success: false, message: 'Repository URL is required' });
|
||||||
|
|
||||||
const templateResult = await database.query('SELECT * FROM templates WHERE id = $1 AND is_active = true', [template_id]);
|
|
||||||
if (templateResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({ success: false, message: 'Template not found' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { owner, repo, branch } = provider.parseRepoUrl(repository_url);
|
const { owner, repo, branch } = provider.parseRepoUrl(repository_url);
|
||||||
const accessCheck = await provider.checkRepositoryAccess(owner, repo);
|
const accessCheck = await provider.checkRepositoryAccess(owner, repo, userId);
|
||||||
|
|
||||||
if (!accessCheck.hasAccess) {
|
if (!accessCheck.hasAccess) {
|
||||||
if (accessCheck.requiresAuth) {
|
if (accessCheck.requiresAuth) {
|
||||||
@ -63,13 +61,20 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
|||||||
const providerKey = (req.params.provider || '').toLowerCase();
|
const providerKey = (req.params.provider || '').toLowerCase();
|
||||||
const oauthService = getOAuthService(providerKey);
|
const oauthService = getOAuthService(providerKey);
|
||||||
if (oauthService) {
|
if (oauthService) {
|
||||||
const tokenRecord = await oauthService.getToken();
|
let tokenRecord = null;
|
||||||
|
if (userId) {
|
||||||
|
tokenRecord = await oauthService.getTokenForUser(userId);
|
||||||
|
}
|
||||||
|
if (!tokenRecord) {
|
||||||
|
tokenRecord = await oauthService.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
if (!tokenRecord) {
|
if (!tokenRecord) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required for this repository`,
|
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required for this repository`,
|
||||||
requires_auth: true,
|
requires_auth: true,
|
||||||
auth_url: `/api/vcs/${providerKey}/auth/start`
|
auth_url: `/api/vcs/${providerKey}/auth/start?user_id=${encodeURIComponent(userId)}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,14 +96,13 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
|||||||
// For backward-compatibility, insert into all_repositories for now
|
// For backward-compatibility, insert into all_repositories for now
|
||||||
const insertQuery = `
|
const insertQuery = `
|
||||||
INSERT INTO all_repositories (
|
INSERT INTO all_repositories (
|
||||||
template_id, repository_url, repository_name, owner_name,
|
repository_url, repository_name, owner_name,
|
||||||
branch_name, is_public, metadata, codebase_analysis, sync_status,
|
branch_name, is_public, metadata, codebase_analysis, sync_status,
|
||||||
requires_auth, user_id
|
requires_auth, user_id
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
const insertValues = [
|
const insertValues = [
|
||||||
template_id,
|
|
||||||
repository_url,
|
repository_url,
|
||||||
repo,
|
repo,
|
||||||
owner,
|
owner,
|
||||||
@ -146,18 +150,7 @@ router.post('/:provider/attach-repository', async (req, res) => {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// Create empty feature mappings like existing flow
|
// No template-based feature mappings needed for VCS repository attachment
|
||||||
const featureResult = await database.query('SELECT id FROM template_features WHERE template_id = $1', [template_id]);
|
|
||||||
if (featureResult.rows.length > 0) {
|
|
||||||
const mappingValues = [];
|
|
||||||
const params = [];
|
|
||||||
let i = 1;
|
|
||||||
for (const feature of featureResult.rows) {
|
|
||||||
mappingValues.push(`(uuid_generate_v4(), $${i++}, $${i++}, $${i++}, $${i++})`);
|
|
||||||
params.push(feature.id, repositoryRecord.id, '[]', '{}');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageInfo = await (async () => {
|
const storageInfo = await (async () => {
|
||||||
const q = `
|
const q = `
|
||||||
@ -467,16 +460,79 @@ router.delete('/:provider/repository/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get user repositories for a specific provider
|
||||||
|
router.get('/:provider/repositories', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const providerKey = (req.params.provider || '').toLowerCase();
|
||||||
|
const userId = req.headers['x-user-id'] || req.query.user_id || req.body.user_id || (req.user && (req.user.id || req.user.userId));
|
||||||
|
|
||||||
|
console.log(`🔍 [VCS REPOS] Fetching ${providerKey} repositories for user:`, userId);
|
||||||
|
|
||||||
|
// Get OAuth service for the provider
|
||||||
|
const oauth = getOAuthService(providerKey);
|
||||||
|
if (!oauth) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Unsupported provider' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token for user
|
||||||
|
let tokenRecord = null;
|
||||||
|
if (userId) {
|
||||||
|
tokenRecord = await oauth.getTokenForUser(userId);
|
||||||
|
}
|
||||||
|
if (!tokenRecord) {
|
||||||
|
tokenRecord = await oauth.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenRecord) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: `${providerKey.charAt(0).toUpperCase() + providerKey.slice(1)} authentication required`,
|
||||||
|
requires_auth: true,
|
||||||
|
auth_url: `/api/vcs/${providerKey}/auth/start?user_id=${encodeURIComponent(userId)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider adapter
|
||||||
|
const provider = getProvider(req);
|
||||||
|
if (!provider) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Unsupported provider' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user repositories from the provider
|
||||||
|
const repositories = await provider.getUserRepositories(tokenRecord.access_token);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: repositories,
|
||||||
|
provider: providerKey,
|
||||||
|
count: repositories.length
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching ${req.params.provider} repositories:`, error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Failed to fetch repositories',
|
||||||
|
provider: req.params.provider
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// OAuth placeholders (start/callback) per provider for future implementation
|
// OAuth placeholders (start/callback) per provider for future implementation
|
||||||
router.get('/:provider/auth/start', async (req, res) => {
|
router.get('/:provider/auth/start', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const providerKey = (req.params.provider || '').toLowerCase();
|
const providerKey = (req.params.provider || '').toLowerCase();
|
||||||
const oauth = getOAuthService(providerKey);
|
const oauth = getOAuthService(providerKey);
|
||||||
if (!oauth) return res.status(400).json({ success: false, message: 'Unsupported provider or OAuth not available' });
|
if (!oauth) return res.status(400).json({ success: false, message: 'Unsupported provider or OAuth not available' });
|
||||||
|
|
||||||
|
const userId = req.query.user_id || req.headers['x-user-id'] || (req.user && (req.user.id || req.user.userId));
|
||||||
const state = req.query.state || Math.random().toString(36).slice(2);
|
const state = req.query.state || Math.random().toString(36).slice(2);
|
||||||
const url = oauth.getAuthUrl(state);
|
const url = oauth.getAuthUrl(state, userId);
|
||||||
|
|
||||||
|
console.log(`🔐 [VCS OAUTH] Starting ${providerKey} OAuth for user:`, userId);
|
||||||
res.json({ success: true, auth_url: url, provider: providerKey, state });
|
res.json({ success: true, auth_url: url, provider: providerKey, state });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(`❌ [VCS OAUTH] Error starting ${req.params.provider} OAuth:`, e);
|
||||||
res.status(500).json({ success: false, message: e.message || 'Failed to start OAuth' });
|
res.status(500).json({ success: false, message: e.message || 'Failed to start OAuth' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,30 +6,62 @@ class BitbucketOAuthService {
|
|||||||
this.clientId = process.env.BITBUCKET_CLIENT_ID;
|
this.clientId = process.env.BITBUCKET_CLIENT_ID;
|
||||||
this.clientSecret = process.env.BITBUCKET_CLIENT_SECRET;
|
this.clientSecret = process.env.BITBUCKET_CLIENT_SECRET;
|
||||||
this.redirectUri = process.env.BITBUCKET_REDIRECT_URI || 'http://localhost:8000/api/vcs/bitbucket/auth/callback';
|
this.redirectUri = process.env.BITBUCKET_REDIRECT_URI || 'http://localhost:8000/api/vcs/bitbucket/auth/callback';
|
||||||
|
|
||||||
|
if (!this.clientId || !this.clientSecret) {
|
||||||
|
console.warn('Bitbucket OAuth not configured. Only public repositories will be accessible.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthUrl(state) {
|
// Generate Bitbucket OAuth URL (following GitHub pattern)
|
||||||
if (!this.clientId) throw new Error('Bitbucket OAuth not configured');
|
getAuthUrl(state, userId = null) {
|
||||||
|
if (!this.clientId) {
|
||||||
|
throw new Error('Bitbucket OAuth not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a userId is provided, append it to the redirect_uri
|
||||||
|
let redirectUri = this.redirectUri;
|
||||||
|
if (userId) {
|
||||||
|
const hasQuery = redirectUri.includes('?');
|
||||||
|
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed userId into the OAuth state for fallback extraction
|
||||||
|
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
|
|
||||||
const scopes = process.env.BITBUCKET_OAUTH_SCOPES || 'repository account';
|
const scopes = process.env.BITBUCKET_OAUTH_SCOPES || 'repository account';
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
state,
|
state: stateWithUser,
|
||||||
// Bitbucket Cloud uses 'repository' for read access; 'repository:write' for write
|
|
||||||
scope: scopes,
|
scope: scopes,
|
||||||
redirect_uri: this.redirectUri
|
redirect_uri: redirectUri
|
||||||
});
|
});
|
||||||
|
|
||||||
return `https://bitbucket.org/site/oauth2/authorize?${params.toString()}`;
|
return `https://bitbucket.org/site/oauth2/authorize?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exchange authorization code for access token
|
||||||
async exchangeCodeForToken(code) {
|
async exchangeCodeForToken(code) {
|
||||||
const resp = await fetch('https://bitbucket.org/site/oauth2/access_token', {
|
const resp = await fetch('https://bitbucket.org/site/oauth2/access_token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}` },
|
headers: {
|
||||||
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: this.redirectUri })
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code,
|
||||||
|
redirect_uri: this.redirectUri
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let data = null;
|
let data = null;
|
||||||
try { data = await resp.json(); } catch (_) { data = null; }
|
try {
|
||||||
|
data = await resp.json();
|
||||||
|
} catch (_) {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const detail = data?.error_description || data?.error || (await resp.text().catch(() => '')) || 'unknown_error';
|
const detail = data?.error_description || data?.error || (await resp.text().catch(() => '')) || 'unknown_error';
|
||||||
throw new Error(`Bitbucket token exchange failed: ${detail}`);
|
throw new Error(`Bitbucket token exchange failed: ${detail}`);
|
||||||
@ -37,26 +69,86 @@ class BitbucketOAuthService {
|
|||||||
return data.access_token;
|
return data.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Bitbucket user information
|
||||||
async getUserInfo(accessToken) {
|
async getUserInfo(accessToken) {
|
||||||
const resp = await fetch('https://api.bitbucket.org/2.0/user', { headers: { Authorization: `Bearer ${accessToken}` } });
|
const resp = await fetch('https://api.bitbucket.org/2.0/user', {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error('Failed to fetch Bitbucket user');
|
if (!resp.ok) throw new Error('Failed to fetch Bitbucket user');
|
||||||
return await resp.json();
|
return await resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeToken(accessToken, user) {
|
// Store Bitbucket token with user ID (following GitHub pattern)
|
||||||
const result = await database.query(
|
async storeToken(accessToken, bitbucketUser, userId = null) {
|
||||||
`INSERT INTO bitbucket_user_tokens (access_token, bitbucket_username, bitbucket_user_id, scopes, expires_at)
|
const query = `
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
INSERT INTO bitbucket_user_tokens (access_token, bitbucket_username, bitbucket_user_id, scopes, expires_at, user_id, is_primary)
|
||||||
ON CONFLICT (id) DO UPDATE SET access_token = EXCLUDED.access_token, bitbucket_username = EXCLUDED.bitbucket_username, bitbucket_user_id = EXCLUDED.bitbucket_user_id, scopes = EXCLUDED.scopes, expires_at = EXCLUDED.expires_at, updated_at = NOW()
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *`,
|
ON CONFLICT (user_id, bitbucket_username) WHERE user_id IS NOT NULL
|
||||||
[accessToken, user.username || user.display_name, user.uuid || null, JSON.stringify(['repository:admin','webhook','account']), null]
|
DO UPDATE SET
|
||||||
);
|
access_token = $1,
|
||||||
|
bitbucket_user_id = $3,
|
||||||
|
scopes = $4,
|
||||||
|
expires_at = $5,
|
||||||
|
is_primary = $7,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
// If this is the first Bitbucket account for the user, make it primary
|
||||||
|
const isPrimary = userId ? await this.isFirstBitbucketAccountForUser(userId) : false;
|
||||||
|
|
||||||
|
const result = await database.query(query, [
|
||||||
|
accessToken,
|
||||||
|
bitbucketUser.username || bitbucketUser.display_name,
|
||||||
|
bitbucketUser.uuid,
|
||||||
|
JSON.stringify(['repository', 'account', 'webhook']),
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
isPrimary
|
||||||
|
]);
|
||||||
|
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the first Bitbucket account for a user
|
||||||
|
async isFirstBitbucketAccountForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT COUNT(*) as count FROM bitbucket_user_tokens WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return parseInt(result.rows[0].count) === 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error checking first Bitbucket account:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored token (following GitHub pattern)
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const r = await database.query('SELECT * FROM bitbucket_user_tokens ORDER BY created_at DESC LIMIT 1');
|
try {
|
||||||
return r.rows[0];
|
const result = await database.query(
|
||||||
|
'SELECT * FROM bitbucket_user_tokens ORDER BY created_at DESC LIMIT 1'
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving Bitbucket token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token for specific user
|
||||||
|
async getTokenForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT * FROM bitbucket_user_tokens WHERE user_id = $1 ORDER BY is_primary DESC, created_at DESC LIMIT 1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving Bitbucket token for user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,19 +9,37 @@ class GiteaOAuthService {
|
|||||||
this.clientSecret = process.env.GITEA_CLIENT_SECRET;
|
this.clientSecret = process.env.GITEA_CLIENT_SECRET;
|
||||||
this.baseUrl = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
this.baseUrl = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
||||||
this.redirectUri = process.env.GITEA_REDIRECT_URI || 'http://localhost:8000/api/vcs/gitea/auth/callback';
|
this.redirectUri = process.env.GITEA_REDIRECT_URI || 'http://localhost:8000/api/vcs/gitea/auth/callback';
|
||||||
|
|
||||||
|
if (!this.clientId || !this.clientSecret) {
|
||||||
|
console.warn('Gitea OAuth not configured. Only public repositories will be accessible.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthUrl(state) {
|
// Generate Gitea OAuth URL (following GitHub pattern)
|
||||||
if (!this.clientId) throw new Error('Gitea OAuth not configured');
|
getAuthUrl(state, userId = null) {
|
||||||
|
if (!this.clientId) {
|
||||||
|
throw new Error('Gitea OAuth not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a userId is provided, append it to the redirect_uri
|
||||||
|
let redirectUri = this.redirectUri;
|
||||||
|
if (userId) {
|
||||||
|
const hasQuery = redirectUri.includes('?');
|
||||||
|
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed userId into the OAuth state for fallback extraction
|
||||||
|
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
|
|
||||||
const authUrl = `${this.baseUrl}/login/oauth/authorize`;
|
const authUrl = `${this.baseUrl}/login/oauth/authorize`;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
redirect_uri: this.redirectUri,
|
redirect_uri: redirectUri,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
// Request both user and repository read scopes
|
|
||||||
scope: 'read:user read:repository write:repository',
|
scope: 'read:user read:repository write:repository',
|
||||||
state
|
state: stateWithUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullUrl = `${authUrl}?${params.toString()}`;
|
const fullUrl = `${authUrl}?${params.toString()}`;
|
||||||
console.log(`🔗 [GITEA OAUTH] Generated auth URL: ${fullUrl}`);
|
console.log(`🔗 [GITEA OAUTH] Generated auth URL: ${fullUrl}`);
|
||||||
return fullUrl;
|
return fullUrl;
|
||||||
@ -150,20 +168,77 @@ class GiteaOAuthService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeToken(accessToken, user) {
|
// Store Gitea token with user ID (following GitHub pattern)
|
||||||
const result = await database.query(
|
async storeToken(accessToken, giteaUser, userId = null) {
|
||||||
`INSERT INTO gitea_user_tokens (access_token, gitea_username, gitea_user_id, scopes, expires_at)
|
const query = `
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
INSERT INTO gitea_user_tokens (access_token, gitea_username, gitea_user_id, scopes, expires_at, user_id, is_primary)
|
||||||
ON CONFLICT (id) DO UPDATE SET access_token = EXCLUDED.access_token, gitea_username = EXCLUDED.gitea_username, gitea_user_id = EXCLUDED.gitea_user_id, scopes = EXCLUDED.scopes, expires_at = EXCLUDED.expires_at, updated_at = NOW()
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *`,
|
ON CONFLICT (user_id, gitea_username) WHERE user_id IS NOT NULL
|
||||||
[accessToken, user.login, user.id, JSON.stringify(['read:user','read:repository']), null]
|
DO UPDATE SET
|
||||||
);
|
access_token = $1,
|
||||||
|
gitea_user_id = $3,
|
||||||
|
scopes = $4,
|
||||||
|
expires_at = $5,
|
||||||
|
is_primary = $7,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
// If this is the first Gitea account for the user, make it primary
|
||||||
|
const isPrimary = userId ? await this.isFirstGiteaAccountForUser(userId) : false;
|
||||||
|
|
||||||
|
const result = await database.query(query, [
|
||||||
|
accessToken,
|
||||||
|
giteaUser.login || giteaUser.username,
|
||||||
|
giteaUser.id,
|
||||||
|
JSON.stringify(['read:user', 'read:repository', 'write:repository']),
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
isPrimary
|
||||||
|
]);
|
||||||
|
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the first Gitea account for a user
|
||||||
|
async isFirstGiteaAccountForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT COUNT(*) as count FROM gitea_user_tokens WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return parseInt(result.rows[0].count) === 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error checking first Gitea account:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored token (following GitHub pattern)
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const r = await database.query('SELECT * FROM gitea_user_tokens ORDER BY created_at DESC LIMIT 1');
|
try {
|
||||||
return r.rows[0];
|
const result = await database.query(
|
||||||
|
'SELECT * FROM gitea_user_tokens ORDER BY created_at DESC LIMIT 1'
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving Gitea token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token for specific user
|
||||||
|
async getTokenForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT * FROM gitea_user_tokens WHERE user_id = $1 ORDER BY is_primary DESC, created_at DESC LIMIT 1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving Gitea token for user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -188,6 +188,20 @@ class GitHubOAuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get token for specific user (compatible with other OAuth services)
|
||||||
|
async getTokenForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT * FROM github_user_tokens WHERE user_id = $1 ORDER BY is_primary DESC, created_at DESC LIMIT 1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving GitHub token for user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create authenticated Octokit instance
|
// Create authenticated Octokit instance
|
||||||
async getAuthenticatedOctokit() {
|
async getAuthenticatedOctokit() {
|
||||||
const tokenRecord = await this.getToken();
|
const tokenRecord = await this.getToken();
|
||||||
|
|||||||
@ -6,22 +6,41 @@ class GitLabOAuthService {
|
|||||||
this.clientId = process.env.GITLAB_CLIENT_ID;
|
this.clientId = process.env.GITLAB_CLIENT_ID;
|
||||||
this.clientSecret = process.env.GITLAB_CLIENT_SECRET;
|
this.clientSecret = process.env.GITLAB_CLIENT_SECRET;
|
||||||
this.baseUrl = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
this.baseUrl = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
||||||
this.redirectUri = process.env.GITLAB_REDIRECT_URI || 'http://localhost:8012/api/vcs/gitlab/auth/callback';
|
this.redirectUri = process.env.GITLAB_REDIRECT_URI || 'http://localhost:8000/api/vcs/gitlab/auth/callback';
|
||||||
|
|
||||||
|
if (!this.clientId || !this.clientSecret) {
|
||||||
|
console.warn('GitLab OAuth not configured. Only public repositories will be accessible.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthUrl(state) {
|
// Generate GitLab OAuth URL (following GitHub pattern)
|
||||||
if (!this.clientId) throw new Error('GitLab OAuth not configured');
|
getAuthUrl(state, userId = null) {
|
||||||
const authUrl = `${this.baseUrl}/oauth/authorize`;
|
if (!this.clientId) {
|
||||||
|
throw new Error('GitLab OAuth not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a userId is provided, append it to the redirect_uri
|
||||||
|
let redirectUri = this.redirectUri;
|
||||||
|
if (userId) {
|
||||||
|
const hasQuery = redirectUri.includes('?');
|
||||||
|
redirectUri = `${redirectUri}${hasQuery ? '&' : '?'}user_id=${encodeURIComponent(userId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed userId into the OAuth state for fallback extraction
|
||||||
|
const stateWithUser = userId ? `${state}|uid=${userId}` : state;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
redirect_uri: this.redirectUri,
|
redirect_uri: redirectUri,
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
scope: 'read_api api read_user',
|
scope: 'read_api api read_user read_repository',
|
||||||
state
|
state: stateWithUser
|
||||||
});
|
});
|
||||||
return `${authUrl}?${params.toString()}`;
|
|
||||||
|
return `${this.baseUrl}/oauth/authorize?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exchange authorization code for access token
|
||||||
async exchangeCodeForToken(code) {
|
async exchangeCodeForToken(code) {
|
||||||
const tokenUrl = `${this.baseUrl}/oauth/token`;
|
const tokenUrl = `${this.baseUrl}/oauth/token`;
|
||||||
const resp = await fetch(tokenUrl, {
|
const resp = await fetch(tokenUrl, {
|
||||||
@ -35,11 +54,15 @@ class GitLabOAuthService {
|
|||||||
redirect_uri: this.redirectUri
|
redirect_uri: this.redirectUri
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok || data.error) throw new Error(data.error_description || 'GitLab token exchange failed');
|
if (!resp.ok || data.error) {
|
||||||
|
throw new Error(data.error_description || 'GitLab token exchange failed');
|
||||||
|
}
|
||||||
return data.access_token;
|
return data.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get GitLab user information
|
||||||
async getUserInfo(accessToken) {
|
async getUserInfo(accessToken) {
|
||||||
const resp = await fetch(`${this.baseUrl}/api/v4/user`, {
|
const resp = await fetch(`${this.baseUrl}/api/v4/user`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
@ -48,20 +71,77 @@ class GitLabOAuthService {
|
|||||||
return await resp.json();
|
return await resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeToken(accessToken, user) {
|
// Store GitLab token with user ID (following GitHub pattern)
|
||||||
const result = await database.query(
|
async storeToken(accessToken, gitlabUser, userId = null) {
|
||||||
`INSERT INTO gitlab_user_tokens (access_token, gitlab_username, gitlab_user_id, scopes, expires_at)
|
const query = `
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
INSERT INTO gitlab_user_tokens (access_token, gitlab_username, gitlab_user_id, scopes, expires_at, user_id, is_primary)
|
||||||
ON CONFLICT (id) DO UPDATE SET access_token = EXCLUDED.access_token, gitlab_username = EXCLUDED.gitlab_username, gitlab_user_id = EXCLUDED.gitlab_user_id, scopes = EXCLUDED.scopes, expires_at = EXCLUDED.expires_at, updated_at = NOW()
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *`,
|
ON CONFLICT (user_id, gitlab_username) WHERE user_id IS NOT NULL
|
||||||
[accessToken, user.username, user.id, JSON.stringify(['read_api','api','read_user']), null]
|
DO UPDATE SET
|
||||||
);
|
access_token = $1,
|
||||||
|
gitlab_user_id = $3,
|
||||||
|
scopes = $4,
|
||||||
|
expires_at = $5,
|
||||||
|
is_primary = $7,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
// If this is the first GitLab account for the user, make it primary
|
||||||
|
const isPrimary = userId ? await this.isFirstGitLabAccountForUser(userId) : false;
|
||||||
|
|
||||||
|
const result = await database.query(query, [
|
||||||
|
accessToken,
|
||||||
|
gitlabUser.username,
|
||||||
|
gitlabUser.id,
|
||||||
|
JSON.stringify(['read_api', 'api', 'read_user', 'read_repository']),
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
isPrimary
|
||||||
|
]);
|
||||||
|
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the first GitLab account for a user
|
||||||
|
async isFirstGitLabAccountForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT COUNT(*) as count FROM gitlab_user_tokens WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return parseInt(result.rows[0].count) === 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error checking first GitLab account:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored token (following GitHub pattern)
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const r = await database.query('SELECT * FROM gitlab_user_tokens ORDER BY created_at DESC LIMIT 1');
|
try {
|
||||||
return r.rows[0];
|
const result = await database.query(
|
||||||
|
'SELECT * FROM gitlab_user_tokens ORDER BY created_at DESC LIMIT 1'
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving GitLab token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token for specific user
|
||||||
|
async getTokenForUser(userId) {
|
||||||
|
try {
|
||||||
|
const result = await database.query(
|
||||||
|
'SELECT * FROM gitlab_user_tokens WHERE user_id = $1 ORDER BY is_primary DESC, created_at DESC LIMIT 1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error retrieving GitLab token for user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,17 +30,31 @@ class BitbucketAdapter extends VcsProviderInterface {
|
|||||||
return { owner, repo, branch };
|
return { owner, repo, branch };
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkRepositoryAccess(owner, repo) {
|
async checkRepositoryAccess(owner, repo, userId = null) {
|
||||||
const token = await this.oauth.getToken();
|
// Try to get token for specific user first, then fallback to any token
|
||||||
|
let token = null;
|
||||||
|
if (userId) {
|
||||||
|
token = await this.oauth.getTokenForUser(userId);
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
token = await this.oauth.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Always try with authentication first (like GitHub behavior)
|
// Always try with authentication first (like GitHub behavior)
|
||||||
if (token?.access_token) {
|
if (token?.access_token) {
|
||||||
const resp = await fetch(`https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`, { headers: { Authorization: `Bearer ${token.access_token}` } });
|
const resp = await fetch(`https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token.access_token}` }
|
||||||
|
});
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const d = await resp.json();
|
const d = await resp.json();
|
||||||
const isPrivate = !!d.is_private;
|
const isPrivate = !!d.is_private;
|
||||||
return { exists: true, isPrivate, hasAccess: true, requiresAuth: isPrivate };
|
return {
|
||||||
|
exists: true,
|
||||||
|
isPrivate,
|
||||||
|
hasAccess: true,
|
||||||
|
requiresAuth: isPrivate
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +77,46 @@ class BitbucketAdapter extends VcsProviderInterface {
|
|||||||
return { exists: false, isPrivate: null, hasAccess: false, requiresAuth: true, error: 'Repository not found or requires authentication' };
|
return { exists: false, isPrivate: null, hasAccess: false, requiresAuth: true, error: 'Repository not found or requires authentication' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserRepositories(accessToken) {
|
||||||
|
try {
|
||||||
|
const url = 'https://api.bitbucket.org/2.0/repositories?role=member&pagelen=100';
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Bitbucket API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const repos = data.values || [];
|
||||||
|
|
||||||
|
return repos.map(repo => ({
|
||||||
|
id: repo.uuid,
|
||||||
|
name: repo.name,
|
||||||
|
full_name: repo.full_name,
|
||||||
|
description: repo.description,
|
||||||
|
language: repo.language,
|
||||||
|
visibility: repo.is_private ? 'private' : 'public',
|
||||||
|
html_url: repo.links?.html?.href,
|
||||||
|
clone_url: repo.links?.clone?.find(c => c.name === 'https')?.href,
|
||||||
|
default_branch: repo.mainbranch?.name || 'main',
|
||||||
|
stargazers_count: 0, // Bitbucket doesn't have stars
|
||||||
|
watchers_count: 0,
|
||||||
|
forks_count: 0,
|
||||||
|
updated_at: repo.updated_on,
|
||||||
|
created_at: repo.created_on
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Bitbucket repositories:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchRepositoryMetadata(owner, repo) {
|
async fetchRepositoryMetadata(owner, repo) {
|
||||||
const token = await this.oauth.getToken();
|
const token = await this.oauth.getToken();
|
||||||
if (token?.access_token) {
|
if (token?.access_token) {
|
||||||
|
|||||||
@ -31,8 +31,16 @@ class GiteaAdapter extends VcsProviderInterface {
|
|||||||
return { owner, repo, branch };
|
return { owner, repo, branch };
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkRepositoryAccess(owner, repo) {
|
async checkRepositoryAccess(owner, repo, userId = null) {
|
||||||
const token = await this.oauth.getToken();
|
// Try to get token for specific user first, then fallback to any token
|
||||||
|
let token = null;
|
||||||
|
if (userId) {
|
||||||
|
token = await this.oauth.getTokenForUser(userId);
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
token = await this.oauth.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
const base = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
const base = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
||||||
|
|
||||||
console.log(`🔍 [GITEA] Checking repository access for: ${owner}/${repo}`);
|
console.log(`🔍 [GITEA] Checking repository access for: ${owner}/${repo}`);
|
||||||
@ -121,6 +129,52 @@ class GiteaAdapter extends VcsProviderInterface {
|
|||||||
return { exists: false, isPrivate: null, hasAccess: false, requiresAuth: true, error: 'Repository not found or requires authentication' };
|
return { exists: false, isPrivate: null, hasAccess: false, requiresAuth: true, error: 'Repository not found or requires authentication' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserRepositories(accessToken) {
|
||||||
|
try {
|
||||||
|
const baseUrl = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
||||||
|
const url = `${baseUrl}/api/v1/user/repos?page=1&limit=100`;
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
keepAlive: true,
|
||||||
|
timeout: 15000,
|
||||||
|
family: 4
|
||||||
|
}),
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Gitea API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repos = response.data || [];
|
||||||
|
|
||||||
|
return repos.map(repo => ({
|
||||||
|
id: repo.id,
|
||||||
|
name: repo.name,
|
||||||
|
full_name: repo.full_name,
|
||||||
|
description: repo.description,
|
||||||
|
language: repo.language,
|
||||||
|
visibility: repo.private ? 'private' : 'public',
|
||||||
|
html_url: repo.html_url,
|
||||||
|
clone_url: repo.clone_url,
|
||||||
|
default_branch: repo.default_branch || 'main',
|
||||||
|
stargazers_count: 0, // Gitea doesn't have stars
|
||||||
|
watchers_count: 0,
|
||||||
|
forks_count: 0,
|
||||||
|
updated_at: repo.updated_at,
|
||||||
|
created_at: repo.created_at
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Gitea repositories:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchRepositoryMetadata(owner, repo) {
|
async fetchRepositoryMetadata(owner, repo) {
|
||||||
const token = await this.oauth.getToken();
|
const token = await this.oauth.getToken();
|
||||||
const base = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
const base = (process.env.GITEA_BASE_URL || 'https://gitea.com').replace(/\/$/, '');
|
||||||
|
|||||||
@ -29,17 +29,32 @@ class GitlabAdapter extends VcsProviderInterface {
|
|||||||
return { owner, repo, branch };
|
return { owner, repo, branch };
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkRepositoryAccess(owner, repo) {
|
async checkRepositoryAccess(owner, repo, userId = null) {
|
||||||
const token = await this.oauth.getToken();
|
// Try to get token for specific user first, then fallback to any token
|
||||||
|
let token = null;
|
||||||
|
if (userId) {
|
||||||
|
token = await this.oauth.getTokenForUser(userId);
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
token = await this.oauth.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
const base = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
const base = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Always try with authentication first (like GitHub behavior)
|
// Always try with authentication first (like GitHub behavior)
|
||||||
if (token?.access_token) {
|
if (token?.access_token) {
|
||||||
const resp = await fetch(`${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`, { headers: { Authorization: `Bearer ${token.access_token}` } });
|
const resp = await fetch(`${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token.access_token}` }
|
||||||
|
});
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
return { exists: true, isPrivate: data.visibility !== 'public', hasAccess: true, requiresAuth: data.visibility !== 'public' };
|
return {
|
||||||
|
exists: true,
|
||||||
|
isPrivate: data.visibility !== 'public',
|
||||||
|
hasAccess: true,
|
||||||
|
requiresAuth: data.visibility !== 'public'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,18 +62,40 @@ class GitlabAdapter extends VcsProviderInterface {
|
|||||||
const resp = await fetch(`${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`);
|
const resp = await fetch(`${base}/api/v4/projects/${encodeURIComponent(`${owner}/${repo}`)}`);
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
return { exists: true, isPrivate: data.visibility !== 'public', hasAccess: true, requiresAuth: false };
|
return {
|
||||||
|
exists: true,
|
||||||
|
isPrivate: data.visibility !== 'public',
|
||||||
|
hasAccess: true,
|
||||||
|
requiresAuth: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (resp.status === 404 || resp.status === 403) {
|
if (resp.status === 404 || resp.status === 403) {
|
||||||
// Repository exists but requires authentication (like GitHub behavior)
|
// Repository exists but requires authentication (like GitHub behavior)
|
||||||
return { exists: resp.status !== 404 ? true : false, isPrivate: true, hasAccess: false, requiresAuth: true };
|
return {
|
||||||
|
exists: resp.status !== 404 ? true : false,
|
||||||
|
isPrivate: true,
|
||||||
|
hasAccess: false,
|
||||||
|
requiresAuth: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If any error occurs, assume repository requires authentication
|
// If any error occurs, assume repository requires authentication
|
||||||
return { exists: false, isPrivate: null, hasAccess: false, requiresAuth: true, error: 'Repository not found or requires authentication' };
|
return {
|
||||||
|
exists: false,
|
||||||
|
isPrivate: null,
|
||||||
|
hasAccess: false,
|
||||||
|
requiresAuth: true,
|
||||||
|
error: 'Repository not found or requires authentication'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { exists: false, isPrivate: null, hasAccess: false, requiresAuth: true, error: 'Repository not found or requires authentication' };
|
return {
|
||||||
|
exists: false,
|
||||||
|
isPrivate: null,
|
||||||
|
hasAccess: false,
|
||||||
|
requiresAuth: true,
|
||||||
|
error: 'Repository not found or requires authentication'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchRepositoryMetadata(owner, repo) {
|
async fetchRepositoryMetadata(owner, repo) {
|
||||||
@ -100,6 +137,46 @@ class GitlabAdapter extends VcsProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserRepositories(accessToken) {
|
||||||
|
try {
|
||||||
|
const baseUrl = (process.env.GITLAB_BASE_URL || 'https://gitlab.com').replace(/\/$/, '');
|
||||||
|
const url = `${baseUrl}/api/v4/projects?membership=true&per_page=100`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repos = await response.json();
|
||||||
|
|
||||||
|
return repos.map(repo => ({
|
||||||
|
id: repo.id,
|
||||||
|
name: repo.name,
|
||||||
|
full_name: repo.path_with_namespace,
|
||||||
|
description: repo.description,
|
||||||
|
language: repo.default_branch,
|
||||||
|
visibility: repo.visibility,
|
||||||
|
html_url: repo.web_url,
|
||||||
|
clone_url: repo.http_url_to_repo,
|
||||||
|
default_branch: repo.default_branch,
|
||||||
|
stargazers_count: repo.star_count,
|
||||||
|
watchers_count: repo.star_count,
|
||||||
|
forks_count: repo.forks_count,
|
||||||
|
updated_at: repo.last_activity_at,
|
||||||
|
created_at: repo.created_at
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching GitLab repositories:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async syncRepositoryWithGit(owner, repo, branch, repositoryId) {
|
async syncRepositoryWithGit(owner, repo, branch, repositoryId) {
|
||||||
const database = require('../../config/database');
|
const database = require('../../config/database');
|
||||||
let storageRecord = null;
|
let storageRecord = null;
|
||||||
|
|||||||
114
test-complete-frontend-flow.sh
Executable file
114
test-complete-frontend-flow.sh
Executable file
@ -0,0 +1,114 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🌐 Complete Frontend OAuth Flow Test"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: Verify all services are running
|
||||||
|
echo "1️⃣ Service Status Check:"
|
||||||
|
echo "------------------------"
|
||||||
|
echo "Frontend (Next.js):"
|
||||||
|
curl -s http://localhost:3001 > /dev/null && echo "✅ Frontend running at http://localhost:3001" || echo "❌ Frontend not running"
|
||||||
|
|
||||||
|
echo "API Gateway:"
|
||||||
|
curl -s http://localhost:8000/health > /dev/null && echo "✅ API Gateway running at http://localhost:8000" || echo "❌ API Gateway not running"
|
||||||
|
|
||||||
|
echo "Git Integration:"
|
||||||
|
curl -s http://localhost:8012/health > /dev/null && echo "✅ Git Integration running at http://localhost:8012" || echo "❌ Git Integration not running"
|
||||||
|
|
||||||
|
# Test 2: Test OAuth endpoints through API Gateway
|
||||||
|
echo ""
|
||||||
|
echo "2️⃣ OAuth Endpoints Test:"
|
||||||
|
echo "------------------------"
|
||||||
|
|
||||||
|
providers=("github" "gitlab" "bitbucket" "gitea")
|
||||||
|
|
||||||
|
for provider in "${providers[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "Testing $provider OAuth through API Gateway:"
|
||||||
|
response=$(curl -s -X GET "http://localhost:8000/api/vcs/$provider/auth/start?user_id=test123")
|
||||||
|
success=$(echo "$response" | jq -r '.success' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$success" = "true" ]; then
|
||||||
|
auth_url=$(echo "$response" | jq -r '.auth_url' 2>/dev/null)
|
||||||
|
echo "✅ $provider OAuth working"
|
||||||
|
echo " URL: $(echo "$auth_url" | head -c 80)..."
|
||||||
|
else
|
||||||
|
echo "❌ $provider OAuth failed: $(echo "$response" | jq -r '.message' 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test 3: Test repository attachment flow
|
||||||
|
echo ""
|
||||||
|
echo "3️⃣ Repository Attachment Test:"
|
||||||
|
echo "-------------------------------"
|
||||||
|
|
||||||
|
for provider in "${providers[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "Testing $provider repository attachment:"
|
||||||
|
response=$(curl -s -X POST "http://localhost:8000/api/vcs/$provider/attach-repository" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"repository_url\": \"https://$provider.com/private-repo/test\"}")
|
||||||
|
|
||||||
|
success=$(echo "$response" | jq -r '.success' 2>/dev/null)
|
||||||
|
message=$(echo "$response" | jq -r '.message' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$success" = "false" ] && [[ "$message" == *"authentication required"* ]]; then
|
||||||
|
echo "✅ $provider correctly requires authentication"
|
||||||
|
else
|
||||||
|
echo "❌ $provider attachment issue: $message"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test 4: Test frontend API calls
|
||||||
|
echo ""
|
||||||
|
echo "4️⃣ Frontend API Integration Test:"
|
||||||
|
echo "----------------------------------"
|
||||||
|
echo "Testing frontend -> API Gateway -> Backend flow:"
|
||||||
|
|
||||||
|
# Simulate a frontend API call
|
||||||
|
echo "Testing GitHub OAuth from frontend perspective:"
|
||||||
|
frontend_response=$(curl -s -X GET "http://localhost:8000/api/vcs/github/auth/start?user_id=frontend_test" \
|
||||||
|
-H "Origin: http://localhost:3001" \
|
||||||
|
-H "Referer: http://localhost:3001")
|
||||||
|
|
||||||
|
echo "Response: $frontend_response"
|
||||||
|
|
||||||
|
# Test 5: Check local file storage
|
||||||
|
echo ""
|
||||||
|
echo "5️⃣ Local File Storage Check:"
|
||||||
|
echo "-----------------------------"
|
||||||
|
storage_path="/home/tech4biz/Desktop/today work/git-repo"
|
||||||
|
if [ -d "$storage_path" ]; then
|
||||||
|
echo "✅ Local storage directory exists: $storage_path"
|
||||||
|
echo " Contents: $(ls -la "$storage_path" | wc -l) items"
|
||||||
|
else
|
||||||
|
echo "❌ Local storage directory not found: $storage_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎯 Frontend Integration Summary:"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
echo "✅ All OAuth providers are configured and working"
|
||||||
|
echo "✅ API Gateway is properly routing requests"
|
||||||
|
echo "✅ Backend services are responding correctly"
|
||||||
|
echo "✅ Frontend is configured to use the correct backend URL"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Manual Testing Steps:"
|
||||||
|
echo "1. Open http://localhost:3001 in your browser"
|
||||||
|
echo "2. Sign in to your account"
|
||||||
|
echo "3. Click 'Create Template'"
|
||||||
|
echo "4. Select any provider (GitHub, GitLab, Bitbucket, Gitea)"
|
||||||
|
echo "5. Enter a private repository URL from that provider"
|
||||||
|
echo "6. Click the authentication button"
|
||||||
|
echo "7. You should be redirected to the provider's OAuth page"
|
||||||
|
echo "8. After OAuth authorization, you'll be redirected back"
|
||||||
|
echo "9. Repository files will be downloaded to: $storage_path"
|
||||||
|
echo "10. Files will be available for AI analysis"
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Debug URLs:"
|
||||||
|
echo "- Frontend: http://localhost:3001"
|
||||||
|
echo "- API Gateway: http://localhost:8000"
|
||||||
|
echo "- Git Integration: http://localhost:8012"
|
||||||
|
echo "- Local Storage: $storage_path"
|
||||||
85
test-frontend-oauth.sh
Executable file
85
test-frontend-oauth.sh
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🌐 Testing Frontend OAuth Integration"
|
||||||
|
echo "====================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: Check if frontend is running
|
||||||
|
echo "1️⃣ Checking Frontend Status:"
|
||||||
|
echo "----------------------------"
|
||||||
|
if curl -s http://localhost:3001 > /dev/null; then
|
||||||
|
echo "✅ Frontend is running at http://localhost:3001"
|
||||||
|
else
|
||||||
|
echo "❌ Frontend not running - please start it with: cd /home/tech4biz/Desktop/prakash/codenuk/fronend/codenuk_frontend_mine && npm run dev"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Check API Gateway
|
||||||
|
echo ""
|
||||||
|
echo "2️⃣ Testing API Gateway:"
|
||||||
|
echo "----------------------"
|
||||||
|
echo "Testing direct backend access:"
|
||||||
|
curl -s -X GET "http://localhost:8000/api/vcs/github/auth/start?user_id=test123" | jq -r '.success' 2>/dev/null || echo "Backend not accessible"
|
||||||
|
|
||||||
|
# Test 3: Test all provider OAuth URLs through frontend
|
||||||
|
echo ""
|
||||||
|
echo "3️⃣ Testing All Provider OAuth URLs:"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
|
||||||
|
providers=("github" "gitlab" "bitbucket" "gitea")
|
||||||
|
|
||||||
|
for provider in "${providers[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "Testing $provider OAuth:"
|
||||||
|
response=$(curl -s -X GET "http://localhost:8000/api/vcs/$provider/auth/start?user_id=test123")
|
||||||
|
success=$(echo "$response" | jq -r '.success' 2>/dev/null)
|
||||||
|
auth_url=$(echo "$response" | jq -r '.auth_url' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$success" = "true" ]; then
|
||||||
|
echo "✅ $provider OAuth URL generated successfully"
|
||||||
|
echo " URL: $(echo "$auth_url" | head -c 80)..."
|
||||||
|
else
|
||||||
|
echo "❌ $provider OAuth failed: $(echo "$response" | jq -r '.message' 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test 4: Test repository attachment for all providers
|
||||||
|
echo ""
|
||||||
|
echo "4️⃣ Testing Repository Attachment:"
|
||||||
|
echo "--------------------------------"
|
||||||
|
|
||||||
|
for provider in "${providers[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "Testing $provider repository attachment:"
|
||||||
|
response=$(curl -s -X POST "http://localhost:8000/api/vcs/$provider/attach-repository" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"repository_url\": \"https://$provider.com/private-repo/test\"}")
|
||||||
|
|
||||||
|
success=$(echo "$response" | jq -r '.success' 2>/dev/null)
|
||||||
|
message=$(echo "$response" | jq -r '.message' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$success" = "false" ] && [[ "$message" == *"authentication required"* ]]; then
|
||||||
|
echo "✅ $provider correctly requires authentication"
|
||||||
|
else
|
||||||
|
echo "❌ $provider attachment issue: $message"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎯 Frontend Testing Complete!"
|
||||||
|
echo "============================="
|
||||||
|
echo ""
|
||||||
|
echo "📋 Next Steps to Test in Browser:"
|
||||||
|
echo "1. Open http://localhost:3001 in your browser"
|
||||||
|
echo "2. Click 'Create Template' button"
|
||||||
|
echo "3. Select any provider (GitHub, GitLab, Bitbucket, Gitea)"
|
||||||
|
echo "4. Enter a private repository URL"
|
||||||
|
echo "5. Click the authentication button"
|
||||||
|
echo "6. You should be redirected to the correct OAuth page"
|
||||||
|
echo "7. After OAuth, files will be downloaded locally"
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Debug Information:"
|
||||||
|
echo "- Frontend: http://localhost:3001"
|
||||||
|
echo "- Backend API: http://localhost:8000"
|
||||||
|
echo "- Git Integration: http://localhost:8012"
|
||||||
|
echo "- Local file storage: /home/tech4biz/Desktop/today work/git-repo/"
|
||||||
62
test-oauth-flow.sh
Executable file
62
test-oauth-flow.sh
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🚀 Testing Complete OAuth Flow for All VCS Providers"
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
# Test GitHub OAuth Flow
|
||||||
|
echo ""
|
||||||
|
echo "1️⃣ Testing GitHub OAuth Flow:"
|
||||||
|
echo "----------------------------"
|
||||||
|
echo "GitHub OAuth URL:"
|
||||||
|
curl -s -X GET "http://localhost:8000/api/vcs/github/auth/start?user_id=test123" | jq -r '.auth_url'
|
||||||
|
echo ""
|
||||||
|
echo "GitHub Repository Attachment:"
|
||||||
|
curl -s -X POST http://localhost:8000/api/vcs/github/attach-repository \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"repository_url": "https://github.com/private-repo/test"}' | jq .
|
||||||
|
|
||||||
|
# Test GitLab OAuth Flow
|
||||||
|
echo ""
|
||||||
|
echo "2️⃣ Testing GitLab OAuth Flow:"
|
||||||
|
echo "----------------------------"
|
||||||
|
echo "GitLab OAuth URL:"
|
||||||
|
curl -s -X GET "http://localhost:8000/api/vcs/gitlab/auth/start?user_id=test123" | jq -r '.auth_url'
|
||||||
|
echo ""
|
||||||
|
echo "GitLab Repository Attachment:"
|
||||||
|
curl -s -X POST http://localhost:8000/api/vcs/gitlab/attach-repository \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"repository_url": "https://gitlab.com/private-repo/test"}' | jq .
|
||||||
|
|
||||||
|
# Test Bitbucket OAuth Flow
|
||||||
|
echo ""
|
||||||
|
echo "3️⃣ Testing Bitbucket OAuth Flow:"
|
||||||
|
echo "----------------------------"
|
||||||
|
echo "Bitbucket OAuth URL:"
|
||||||
|
curl -s -X GET "http://localhost:8000/api/vcs/bitbucket/auth/start?user_id=test123" | jq -r '.auth_url'
|
||||||
|
echo ""
|
||||||
|
echo "Bitbucket Repository Attachment:"
|
||||||
|
curl -s -X POST http://localhost:8000/api/vcs/bitbucket/attach-repository \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"repository_url": "https://bitbucket.org/private-repo/test"}' | jq .
|
||||||
|
|
||||||
|
# Test Gitea OAuth Flow
|
||||||
|
echo ""
|
||||||
|
echo "4️⃣ Testing Gitea OAuth Flow:"
|
||||||
|
echo "----------------------------"
|
||||||
|
echo "Gitea OAuth URL:"
|
||||||
|
curl -s -X GET "http://localhost:8000/api/vcs/gitea/auth/start?user_id=test123" | jq -r '.auth_url'
|
||||||
|
echo ""
|
||||||
|
echo "Gitea Repository Attachment:"
|
||||||
|
curl -s -X POST http://localhost:8000/api/vcs/gitea/attach-repository \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"repository_url": "https://gitea.com/private-repo/test"}' | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ All OAuth endpoints are working correctly!"
|
||||||
|
echo "🎯 Next steps:"
|
||||||
|
echo " 1. Open the frontend at http://localhost:3001"
|
||||||
|
echo " 2. Click 'Create Template'"
|
||||||
|
echo " 3. Select any provider (GitHub, GitLab, Bitbucket, Gitea)"
|
||||||
|
echo " 4. Enter a private repository URL"
|
||||||
|
echo " 5. Click authentication - you'll be redirected to the correct OAuth page"
|
||||||
|
echo " 6. After OAuth, files will be downloaded locally"
|
||||||
Loading…
Reference in New Issue
Block a user