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"
|
DATABASE_URL="postgresql://user:password@localhost:5432/proj_dash?schema=public"
|
||||||
JWT_SECRET="your-jwt-secret"
|
JWT_SECRET="your-jwt-secret-here-use-openssl-rand-base64-32"
|
||||||
GITHUB_CLIENT_ID="your-github-client-id"
|
|
||||||
GITHUB_CLIENT_SECRET="your-github-client-secret"
|
# 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"
|
FRONTEND_URL="http://localhost:5173"
|
||||||
UPSTASH_REDIS_URL="http://localhost:6379"
|
UPSTASH_REDIS_URL="http://localhost:6379"
|
||||||
UPSTASH_REDIS_TOKEN="your-upstash-redis-token"
|
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',
|
'/login',
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
description: 'Login with GitHub OAuth code',
|
description: 'Login with GitHub App OAuth code (user-to-server authentication)',
|
||||||
tags: ['auth'],
|
tags: ['auth'],
|
||||||
body: loginBodySchema,
|
body: loginBodySchema,
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@ -88,7 +88,7 @@ GitHub API rate limits apply. Authenticated requests: 5,000/hour.
|
|||||||
description: 'Local development server',
|
description: 'Local development server',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: 'https://api.projectdash.example.com',
|
url: 'https://zany-couscous-jg4x4q5q6gj2p945-3333.app.github.dev/',
|
||||||
description: 'Production server',
|
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
|
// Define a type for the repository list response data for clarity
|
||||||
type ReposListForAuthenticatedUserResponse = Endpoints['GET /user/repos']['response']['data']
|
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> {
|
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',
|
'https://github.com/login/oauth/access_token',
|
||||||
{
|
{
|
||||||
client_id: process.env.GITHUB_CLIENT_ID,
|
client_id: process.env['GITHUB_APP_CLIENT_ID'],
|
||||||
client_secret: process.env.GITHUB_CLIENT_SECRET,
|
client_secret: process.env['GITHUB_APP_CLIENT_SECRET'],
|
||||||
code,
|
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
|
console.log('GitHub OAuth response status:', response.status)
|
||||||
return response.data?.access_token
|
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> {
|
export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
|
||||||
@ -45,15 +77,27 @@ export class GitHubService {
|
|||||||
|
|
||||||
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
||||||
const cacheKey = `repos:${this.username}`
|
const cacheKey = `repos:${this.username}`
|
||||||
const cachedRepos = await redis.get(cacheKey)
|
|
||||||
|
|
||||||
|
// Try to get from cache, but don't fail if Redis is unavailable
|
||||||
|
try {
|
||||||
|
const cachedRepos = await redis.get(cacheKey)
|
||||||
if (cachedRepos) {
|
if (cachedRepos) {
|
||||||
|
console.log('Cache hit for repositories')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return JSON.parse(cachedRepos as string)
|
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()
|
const { data } = await this.octokit.repos.listForAuthenticatedUser()
|
||||||
|
|
||||||
|
// 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
|
await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }) // Cache for 1 hour
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to cache repositories:', error)
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user