feat: api测试完成
This commit is contained in:
parent
89e7e5794a
commit
58ca9f5fe9
16
.env.example
16
.env.example
@ -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
145
GITHUB_APP_SETUP.md
Normal 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)
|
||||
44
prisma/migrations/20251027054428_init/migration.sql
Normal file
44
prisma/migrations/20251027054428_init/migration.sql
Normal 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;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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"
|
||||
@ -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: {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user