feat: api测试完成

This commit is contained in:
grtsinry43 2025-10-27 05:59:24 +00:00
parent 89e7e5794a
commit 58ca9f5fe9
7 changed files with 260 additions and 16 deletions

View File

@ -1,7 +1,15 @@
DATABASE_URL="your-database-url"
JWT_SECRET="your-jwt-secret"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
DATABASE_URL="postgresql://user:password@localhost:5432/proj_dash?schema=public"
JWT_SECRET="your-jwt-secret-here-use-openssl-rand-base64-32"
# GitHub App Configuration (modern approach with fine-grained permissions)
# See GITHUB_APP_SETUP.md for detailed setup instructions
GITHUB_APP_ID="your-github-app-id"
GITHUB_APP_CLIENT_ID="your-github-app-client-id"
GITHUB_APP_CLIENT_SECRET="your-github-app-client-secret"
# Optional: For App-level authentication (not required for user auth)
# GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
FRONTEND_URL="http://localhost:5173"
UPSTASH_REDIS_URL="http://localhost:6379"
UPSTASH_REDIS_TOKEN="your-upstash-redis-token"

145
GITHUB_APP_SETUP.md Normal file
View File

@ -0,0 +1,145 @@
# GitHub App Setup Guide
This project uses **GitHub App** for authentication, which provides better security and more fine-grained permissions compared to OAuth Apps.
## Why GitHub App?
- **Fine-grained permissions**: Request only the permissions you need
- **Better security**: Separate user authentication from app installation
- **Modern approach**: Recommended by GitHub for new applications
- **Flexible**: Support both user authentication and app-level operations
## Creating a GitHub App
### 1. Navigate to GitHub App Settings
Go to [GitHub Developer Settings](https://github.com/settings/apps) and click **New GitHub App**.
### 2. Fill in Basic Information
- **GitHub App name**: Your app name (e.g., "Project Dashboard Dev")
- **Homepage URL**: `http://localhost:5173` (for development)
- **Callback URL**: `http://localhost:5173/callback`
- **Webhook**: Uncheck "Active" (unless you need webhooks)
### 3. Configure Permissions
Under **Repository permissions**, select:
- **Contents**: Read-only (to read repository data)
- **Metadata**: Read-only (required, automatically enabled)
- **Commit statuses**: Read-only (for commit statistics)
Under **Account permissions**, select:
- **Email addresses**: Read-only (to get user email)
### 4. Where can this GitHub App be installed?
Choose:
- **Any account** (allows any user to authenticate)
### 5. Create the App
Click **Create GitHub App**.
### 6. Get Your Credentials
After creation, you'll see:
1. **App ID**: Found at the top of the app settings page
2. **Client ID**: Under "About" section
3. **Client secrets**: Click "Generate a new client secret"
### 7. Update Your `.env` File
Copy the credentials to your `.env` file:
```env
GITHUB_APP_ID="123456"
GITHUB_APP_CLIENT_ID="Iv1.abc123def456"
GITHUB_APP_CLIENT_SECRET="your-client-secret-here"
```
## User Authentication Flow
The authentication flow for GitHub Apps is similar to OAuth Apps:
1. **Frontend**: Redirect user to GitHub authorization URL:
```
https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID
```
2. **GitHub**: User authorizes the app and is redirected back with a `code`
3. **Frontend**: Send the `code` to your backend API:
```
POST /auth/login
{ "code": "received_code" }
```
4. **Backend**:
- Exchange code for access token
- Fetch user information
- Generate JWT token
- Return to frontend
## Frontend Integration Example
```typescript
// Redirect to GitHub for authorization
const clientId = 'YOUR_GITHUB_APP_CLIENT_ID'
const redirectUri = 'http://localhost:5173/callback'
const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}`
window.location.href = authUrl
// In your callback page, extract the code
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
// Send to backend
const response = await fetch('http://localhost:3333/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
})
const { data } = await response.json()
const token = data.token // Use this JWT for subsequent API calls
```
## Differences from OAuth Apps
| Feature | OAuth App | GitHub App |
| ----------- | --------------------- | ----------------------------------------- |
| Permissions | Coarse-grained scopes | Fine-grained permissions |
| Token Type | User access token | User access token + Installation token |
| Rate Limits | 5,000 requests/hour | 5,000 requests/hour (user) + 15,000 (app) |
| Recommended | Legacy | **✓ Modern** |
## Troubleshooting
### "Bad verification code" error
- Ensure the code hasn't expired (codes are single-use and expire after 10 minutes)
- Check that your Client ID and Client Secret are correct
### "Not found" error
- Verify your GitHub App is set to "Any account" installation
- Check that the callback URL matches your frontend URL
### API Rate Limiting
- User authentication has 5,000 requests/hour per user
- Consider implementing Redis caching (already included in this project)
## Additional Resources
- [GitHub Apps Documentation](https://docs.github.com/en/apps)
- [User-to-Server Authentication](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app)
- [GitHub App Permissions](https://docs.github.com/en/rest/overview/permissions-required-for-github-apps)

View File

@ -0,0 +1,44 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"githubId" TEXT NOT NULL,
"username" TEXT NOT NULL,
"avatarUrl" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Repository" (
"id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"owner" TEXT NOT NULL,
CONSTRAINT "Repository_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PullRequest" (
"id" TEXT NOT NULL,
"number" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"state" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL,
"closed_at" TIMESTAMP(3),
"repositoryId" INTEGER NOT NULL,
"authorId" TEXT NOT NULL,
CONSTRAINT "PullRequest_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_githubId_key" ON "User"("githubId");
-- CreateIndex
CREATE UNIQUE INDEX "PullRequest_repositoryId_number_key" ON "PullRequest"("repositoryId", "number");
-- AddForeignKey
ALTER TABLE "PullRequest" ADD CONSTRAINT "PullRequest_repositoryId_fkey" FOREIGN KEY ("repositoryId") REFERENCES "Repository"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PullRequest" ADD CONSTRAINT "PullRequest_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -23,7 +23,7 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
'/login',
{
schema: {
description: 'Login with GitHub OAuth code',
description: 'Login with GitHub App OAuth code (user-to-server authentication)',
tags: ['auth'],
body: loginBodySchema,
response: {

View File

@ -88,7 +88,7 @@ GitHub API rate limits apply. Authenticated requests: 5,000/hour.
description: 'Local development server',
},
{
url: 'https://api.projectdash.example.com',
url: 'https://zany-couscous-jg4x4q5q6gj2p945-3333.app.github.dev/',
description: 'Production server',
},
],

View File

@ -9,12 +9,30 @@ const GITHUB_API_BASE_URL = 'https://api.github.com'
// Define a type for the repository list response data for clarity
type ReposListForAuthenticatedUserResponse = Endpoints['GET /user/repos']['response']['data']
// GitHub OAuth token response type
interface GitHubTokenResponse {
access_token: string
token_type: string
scope: string
error?: string
error_description?: string
}
/**
* Exchange GitHub OAuth code for access token
* Works with both OAuth Apps and GitHub Apps (user-to-server flow)
* @see https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app
*/
export async function exchangeCodeForToken(code: string): Promise<string> {
const response = await axios.post(
console.log('Exchanging code for token...')
console.log('Client ID:', process.env['GITHUB_APP_CLIENT_ID'])
console.log('Code:', code.substring(0, 10) + '...')
const response = await axios.post<GitHubTokenResponse>(
'https://github.com/login/oauth/access_token',
{
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
client_id: process.env['GITHUB_APP_CLIENT_ID'],
client_secret: process.env['GITHUB_APP_CLIENT_SECRET'],
code,
},
{
@ -22,8 +40,22 @@ export async function exchangeCodeForToken(code: string): Promise<string> {
}
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access
return response.data?.access_token
console.log('GitHub OAuth response status:', response.status)
console.log('GitHub OAuth response data:', response.data)
// Check for errors in the response
if (response.data.error) {
throw new Error(
`GitHub OAuth error: ${response.data.error} - ${response.data.error_description ?? ''}`
)
}
if (!response.data.access_token) {
console.error('GitHub OAuth response:', response.data)
throw new Error('No access token received from GitHub')
}
return response.data.access_token
}
export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
@ -45,15 +77,27 @@ export class GitHubService {
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
const cacheKey = `repos:${this.username}`
const cachedRepos = await redis.get(cacheKey)
if (cachedRepos) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(cachedRepos as string)
// Try to get from cache, but don't fail if Redis is unavailable
try {
const cachedRepos = await redis.get(cacheKey)
if (cachedRepos) {
console.log('Cache hit for repositories')
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(cachedRepos as string)
}
} catch (error) {
console.warn('Redis cache unavailable, fetching from GitHub API directly:', error)
}
const { data } = await this.octokit.repos.listForAuthenticatedUser()
await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }) // Cache for 1 hour
// Try to cache, but don't fail if Redis is unavailable
try {
await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }) // Cache for 1 hour
} catch (error) {
console.warn('Failed to cache repositories:', error)
}
return data
}