This commit is contained in:
lijiayang050403 2025-10-24 04:55:08 +00:00
parent 15d61a005f
commit 57f964744f
10 changed files with 1416 additions and 211 deletions

553
design.md Normal file
View File

@ -0,0 +1,553 @@
好的!我理解了,你需要:
1. **前端**现代化框架React/Vue
2. **后端**:独立的轻量后端(不用 Next.js API Routes
3. **部署**:都要简单方便
让我重新设计一个最佳方案!🎯 **最终推荐架构**
* * *
### **技术选型**
前端 (SPA):
- Framework: Vite + React 18 + TypeScript
- UI: Tailwind CSS + shadcn/ui
- State: Zustand + TanStack Query
- Charts: Recharts + D3.js
- 部署: Cloudflare Pages / Vercel (静态托管)
后端 (API):
- Framework: Fastify + TypeScript (比Express快2倍)
- GitHub SDK: Octokit
- Database: PostgreSQL (Supabase免费托管)
- Cache: Upstash Redis (Serverless)
- 部署: Railway.app ⭐ (一键部署,免费额度够用)
认证:
- GitHub OAuth 2.0
- JWT Token
实时通信:
- Server-Sent Events (SSE) - 比WebSocket简单
- 或 Pusher (免费版)
* * *
🏗️ **系统架构图**
┌─────────────────────────────────────────────────────────────┐
│ 用户浏览器 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ React SPA (Vite) │ │
│ │ │ │
│ │ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │ │
│ │ │Dashboard │ │Repository │ │Analytics │ │ │
│ │ │ Page │ │Detail Page│ │ Page │ │ │
│ │ └────┬─────┘ └─────┬─────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────────┴────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────▼──────────┐ │ │
│ │ │ TanStack Query │ (数据获取层) │ │
│ │ │ + Axios/Fetch │ │ │
│ │ └─────────┬──────────┘ │ │
│ └──────────────────────┼───────────────────────────────┘ │
└─────────────────────────┼──────────────────────────────────┘
HTTPS (REST API)
┌─────────────────────────▼──────────────────────────────────┐
│ 后端服务器 (Railway) │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Fastify API Server (Node.js) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ Auth │ │ Repos │ │ Webhooks │ │ │
│ │ │ /login │ │ /repos/* │ │ /webhooks/* │ │ │
│ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ └─────────────┴────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────▼──────────┐ │ │
│ │ │ Service Layer │ │ │
│ │ │ • GitHubService │ │ │
│ │ │ • CacheService │ │ │
│ │ │ • WebhookService │ │ │
│ │ └──────────┬──────────┘ │ │
│ └─────────────────────┼────────────────────────────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────▼─────┐ ┌────▼────┐ ┌──────▼──────┐ │
│ │ Octokit │ │ Prisma │ │ Redis Cache │ │
│ │ GitHub │ │ ORM │ │ (Upstash) │ │
│ │ API │ └────┬────┘ └─────────────┘ │
│ └──────────┘ │ │
└───────────────────────┼─────────────────────────────────────┘
┌──────▼──────┐
│ PostgreSQL │
│ (Supabase) │
└─────────────┘
┌────────────────────────────────────────────┐
│ 外部服务 │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ GitHub API │ │ GitHub Webhooks │ │
│ │ (公开接口) │ │ (实时事件推送) │ │
│ └──────────────┘ └──────────────────┘ │
└────────────────────────────────────────────┘
* * *
📂 **项目结构**
github-dashboard/
├── frontend/ # 前端项目 (Vite)
│ ├── src/
│ │ ├── components/
│ │ │ ├── dashboard/
│ │ │ │ ├── StatsCard.tsx
│ │ │ │ ├── ActivityChart.tsx
│ │ │ │ └── RepoList.tsx
│ │ │ ├── charts/
│ │ │ └── ui/ # shadcn/ui 组件
│ │ ├── pages/
│ │ │ ├── Dashboard.tsx
│ │ │ ├── RepoDetail.tsx
│ │ │ └── Analytics.tsx
│ │ ├── hooks/
│ │ │ ├── useRepos.ts
│ │ │ └── useRepoStats.ts
│ │ ├── lib/
│ │ │ ├── api.ts # API 请求封装
│ │ │ └── auth.ts # 认证逻辑
│ │ ├── stores/
│ │ │ └── authStore.ts # Zustand store
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── package.json
│ ├── vite.config.ts
│ └── tailwind.config.js
├── backend/ # 后端项目 (Fastify)
│ ├── src/
│ │ ├── routes/
│ │ │ ├── auth.ts # POST /auth/login
│ │ │ ├── repos.ts # GET /repos, /repos/:id
│ │ │ ├── webhooks.ts # POST /webhooks/github
│ │ │ └── stats.ts # GET /stats/*
│ │ ├── services/
│ │ │ ├── github.service.ts
│ │ │ ├── cache.service.ts
│ │ │ └── webhook.service.ts
│ │ ├── middlewares/
│ │ │ ├── auth.middleware.ts
│ │ │ └── error.middleware.ts
│ │ ├── prisma/
│ │ │ └── schema.prisma
│ │ ├── types/
│ │ │ └── index.ts
│ │ ├── utils/
│ │ │ └── logger.ts
│ │ ├── app.ts # Fastify 实例
│ │ └── server.ts # 启动文件
│ ├── package.json
│ ├── tsconfig.json
│ └── .env.example
└── README.md
* * *
🚀 **部署方案**
### **前端部署Cloudflare Pages**
# 1. 构建
cd frontend
npm run build
# 2. Cloudflare Pages 自动部署
# - 连接 GitHub 仓库
# - 构建命令: npm run build
# - 输出目录: dist
# - 自动部署: 推送到 main 分支即触发
**优势:**
* ✅ 完全免费
* ✅ 全球 CDN超快
* ✅ 自动 HTTPS
* ✅ 无限带宽
* ✅ 支持自定义域名
* * *
### **后端部署Railway.app** ⭐⭐⭐
# 1. 安装 Railway CLI
npm install -g @railway/cli
# 2. 登录
railway login
# 3. 初始化项目
cd backend
railway init
# 4. 部署
railway up
# 5. 添加数据库
railway add postgresql
# 6. 设置环境变量
railway variables set GITHUB_CLIENT_ID=xxx
railway variables set GITHUB_CLIENT_SECRET=xxx
**Railway 配置文件 (`railway.json`)**: { "$schema": "https://railway.app/railway.schema.json", "build": { "builder": "NIXPACKS" }, "deploy": { "startCommand": "npm run start", "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 10 } }
**优势:**
* ✅ 免费额度:$5/月(足够小项目)
* ✅ 一键部署,零配置
* ✅ 自动 HTTPS
* ✅ 内置 PostgreSQL
* ✅ 环境变量管理
* ✅ 自动 Git 同步部署
* * *
### **替代方案对比**
| 平台 | 免费额度 | 部署难度 | 启动速度 | 推荐度 |
| --- | --- | --- | --- | --- |
| **Railway** | $5/月 | ⭐⭐⭐⭐⭐ | 快 | ⭐⭐⭐⭐⭐ |
| Render | 750h/月 | ⭐⭐⭐⭐ | 慢(冷启动) | ⭐⭐⭐⭐ |
| Fly.io | 3个免费实例 | ⭐⭐⭐ | 快 | ⭐⭐⭐⭐ |
| Heroku | 无免费版 | ⭐⭐⭐⭐ | 中等 | ⭐⭐ |
* * *
💻 **核心代码示例**
### **后端 - Fastify 入口 (`backend/src/app.ts`)**
import Fastify from 'fastify';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import { authRoutes } from './routes/auth';
import { repoRoutes } from './routes/repos';
import { webhookRoutes } from './routes/webhooks';
export function createApp() {
const app = Fastify({
logger: true,
});
// 插件
app.register(cors, {
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
});
app.register(jwt, {
secret: process.env.JWT_SECRET!,
});
// 路由
app.register(authRoutes, { prefix: '/api/auth' });
app.register(repoRoutes, { prefix: '/api/repos' });
app.register(webhookRoutes, { prefix: '/api/webhooks' });
// 健康检查
app.get('/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
return app;
}
### **后端 - GitHub Service (`backend/src/services/github.service.ts`)**
import { Octokit } from '@octokit/rest';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_URL!,
token: process.env.UPSTASH_REDIS_TOKEN!,
});
export class GitHubService {
private octokit: Octokit;
constructor(accessToken: string) {
this.octokit = new Octokit({ auth: accessToken });
}
async getRepositories() {
const cacheKey = 'repos:list';
// 尝试从缓存读取
const cached = await redis.get(cacheKey);
if (cached) return cached;
// 从 GitHub API 获取
const { data } = await this.octokit.repos.listForAuthenticatedUser({
sort: 'updated',
per_page: 100,
});
// 缓存5分钟
await redis.set(cacheKey, data, { ex: 300 });
return data;
}
async getRepositoryStats(owner: string, repo: string) {
const cacheKey = `repo:${owner}/${repo}:stats`;
const cached = await redis.get(cacheKey);
if (cached) return cached;
const [repoData, issues, pulls, commits] = await Promise.all([
this.octokit.repos.get({ owner, repo }),
this.octokit.issues.listForRepo({ owner, repo, state: 'open' }),
this.octokit.pulls.list({ owner, repo, state: 'open' }),
this.octokit.repos.listCommits({
owner,
repo,
per_page: 100,
since: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
}),
]);
const stats = {
name: repoData.data.name,
stars: repoData.data.stargazers_count,
forks: repoData.data.forks_count,
openIssues: issues.data.length,
openPRs: pulls.data.length,
commitsLastMonth: commits.data.length,
language: repoData.data.language,
updatedAt: repoData.data.updated_at,
};
await redis.set(cacheKey, stats, { ex: 300 });
return stats;
}
}
### **前端 - API 客户端 (`frontend/src/lib/api.ts`)**
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
timeout: 10000,
});
// 请求拦截器:添加 token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:处理错误
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
### **前端 - 数据获取 Hook (`frontend/src/hooks/useRepos.ts`)**
import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api';
interface Repository {
id: number;
name: string;
full_name: string;
description: string;
stargazers_count: number;
open_issues_count: number;
language: string;
}
export function useRepositories() {
return useQuery({
queryKey: ['repositories'],
queryFn: async () => {
const { data } = await api.get<Repository[]>('/repos');
return data;
},
staleTime: 5 * 60 * 1000, // 5分钟
refetchOnWindowFocus: false,
});
}
export function useRepositoryStats(owner: string, repo: string) {
return useQuery({
queryKey: ['repo-stats', owner, repo],
queryFn: async () => {
const { data } = await api.get(`/repos/${owner}/${repo}/stats`);
return data;
},
enabled: !!owner && !!repo,
staleTime: 5 * 60 * 1000,
});
}
* * *
🔐 **认证流程**
┌────────┐ ┌──────────┐ ┌─────────┐
│ 前端 │ │ 后端 │ │ GitHub │
└───┬────┘ └────┬─────┘ └────┬────┘
│ │ │
│ 1. 点击 "Login with GitHub" │ │
├──────────────────────────>│ │
│ │ │
│ 2. 重定向到 GitHub OAuth │ │
├───────────────────────────┼─────────────────────────>│
│ │ │
│ 3. 用户授权 │ │
│ │<──────────────────────────┤
│ │ │
│ 4. GitHub 回调 + code │ │
<───────────────────────────┤ │
│ │ │
│ 5. 发送 code 到后端 │ │
├──────────────────────────>│ │
│ │ │
│ │ 6. 用 code 换 access_token│
│ ├─────────────────────────>│
│ │ │
│ │<──────────────────────────┤
│ │ │
│ 7. 返回 JWT token │ │
<───────────────────────────┤ │
│ │ │
│ 8. 保存 token 到 localStorage │
│ │ │
* * *
📊 **数据流**
前端 Dashboard 页面加载:
1. 检查 localStorage 是否有 token
├─ 无 → 跳转登录页
└─ 有 → 继续
2. 发起并行请求TanStack Query:
├─ GET /api/repos (获取仓库列表)
├─ GET /api/stats/overview (总体统计)
└─ GET /api/stats/activity (活动数据)
3. 后端处理:
├─ 验证 JWT token
├─ 检查 Redis 缓存
│ ├─ 命中 → 直接返回
│ └─ 未命中 → 调用 GitHub API + 写入缓存
└─ 返回数据
4. 前端渲染:
├─ TanStack Query 自动管理加载/错误状态
├─ 渲染图表Recharts
└─ 显示数据卡片
* * *
🎯 **开发步骤**
### **1. 初始化项目**
# 创建项目目录
mkdir github-dashboard
cd github-dashboard
# 初始化前端
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm install @tanstack/react-query axios zustand
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# 初始化后端
cd ..
mkdir backend && cd backend
npm init -y
npm install fastify @fastify/cors @fastify/jwt
npm install @octokit/rest @upstash/redis @prisma/client
npm install -D typescript @types/node tsx prisma
npx prisma init
### **2. 配置 Railway 部署**
cd backend
# 创建 railway.json
cat > railway.json << EOF
{
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "npm run start"
}
}
EOF
# 部署
railway login
railway init
railway up
### **3. 配置 Cloudflare Pages**
cd frontend
# 构建测试
npm run build
# 在 Cloudflare Pages Dashboard:
# 1. 连接 GitHub 仓库
# 2. 设置构建命令: npm run build
# 3. 设置输出目录: dist
# 4. 添加环境变量: VITE_API_URL=你的Railway后端URL
* * *
🎉 **总结**
这个架构的优势:
**前后端分离** - 各自独立部署和扩展✅ **轻量高效** - Fastify 性能优秀Vite 开发体验好✅ **部署简单** - Railway + Cloudflare Pages 都是一键部署✅ **成本低廉** - 免费额度足够个人/小团队使用✅ **可扩展性** - 需要时可轻松升级到付费版✅ **现代化** - TypeScript 全栈,类型安全
需要我提供:
1. ✅ 完整的项目初始化脚本?
2. ✅ Prisma Schema 数据库设计?
3. ✅ 前端 Dashboard 组件代码?
4. ✅ 详细的部署文档?
选择任意方向深入!🚀

715
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"main": "dist/server.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only --exit-child src/server.ts",
"build": "tsc",
"build": "prisma generate && tsc",
"start": "node dist/server.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev"
@ -14,19 +14,24 @@
"author": "Google LLC",
"license": "Apache-2.0",
"dependencies": {
"dotenv": "^17.2.3",
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@octokit/rest": "^22.0.0",
"@fastify/swagger": "^9.5.2",
"@octokit/rest": "^19.0.13",
"@prisma/client": "^6.18.0",
"@upstash/redis": "^1.35.6",
"fastify": "^5.6.1"
"axios": "^1.12.2",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"fastify-type-provider-zod": "^6.0.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/node": "^20.12.12",
"nodemon": "^3.1.0",
"pino-pretty": "^13.1.2",
"prisma": "^6.18.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
}
}
}

View File

@ -1,43 +1,67 @@
import { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { GitHubService } from '../services/github.service';
import { authMiddleware } from '../middlewares/auth.middleware';
// Schema for the URL parameters
const repoParamsSchema = {
type: 'object',
required: ['owner', 'repo'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
},
} as const; // Using 'as const' for stronger type inference
// Schema for request parameters
const repoParamsSchema = z.object({
owner: z.string(),
repo: z.string(),
});
// Schema for the response of the repository stats endpoint
const repoStatsSchema = {
type: 'object',
properties: {
stars: { type: 'number' },
forks: { type: 'number' },
openIssues: { type: 'number' },
commits: { type: 'number' },
releases: { type: 'number' },
contributors: { type: 'number' },
},
} as const;
// Schema for repository statistics response
const repoStatsSchema = z.object({
stars: z.number(),
forks: z.number(),
openIssues: z.number(),
commits: z.number(),
releases: z.number(),
contributors: z.number(),
});
// Schema for repository list response
const repoListItemSchema = z.object({
id: z.number(),
name: z.string(),
full_name: z.string(),
private: z.boolean(),
html_url: z.string().url(),
description: z.string().nullable(),
stargazers_count: z.number(),
watchers_count: z.number(),
forks_count: z.number(),
language: z.string().nullable(),
owner: z.object({
login: z.string(),
avatar_url: z.string().url(),
}),
});
const reposListResponseSchema = z.array(repoListItemSchema);
export async function repoRoutes(app: FastifyInstance) {
// All routes in this plugin require authentication
app.addHook('preHandler', authMiddleware);
// This route retrieves all repositories for the authenticated user.
// The response type is inferred from Octokit, so a detailed schema is omitted for brevity,
// but in a real-world scenario, you might want to define the properties you actually use.
app.get('/', async (request, reply) => {
const githubService = new GitHubService(request.user.accessToken);
const repos = await githubService.getRepositories();
reply.send(repos);
});
// Route to list repositories for the authenticated user
app.get(
'/',
{
schema: {
response: {
200: reposListResponseSchema,
},
},
},
async (request, reply) => {
const { accessToken, username } = request.user!;
const githubService = new GitHubService(accessToken, username);
const repos = await githubService.getRepositories();
return repos;
},
);
// This route gets specific stats for a single repository.
// Route to get statistics for a specific repository
app.get(
'/:owner/:repo/stats',
{
@ -49,13 +73,10 @@ export async function repoRoutes(app: FastifyInstance) {
},
},
async (request, reply) => {
// request.params is now strongly typed based on repoParamsSchema!
const { owner, repo } = request.params;
const githubService = new GitHubService(request.user.accessToken);
const { owner, repo } = request.params as z.infer<typeof repoParamsSchema>;
const { accessToken, username } = request.user!;
const githubService = new GitHubService(accessToken, username);
const stats = await githubService.getRepositoryStats(owner, repo);
// The return type is validated against repoStatsSchema
return stats;
},
);

View File

@ -1,5 +1,21 @@
import { FastifyInstance } from 'fastify';
import { StatsService } from '../services/stats.service';
import { authMiddleware } from '../middlewares/auth.middleware';
export async function statsRoutes(app: FastifyInstance) {
// TODO: Implement stats routes
app.addHook('preHandler', authMiddleware);
app.get('/overview', async (request, reply) => {
const { accessToken, username } = request.user!;
const statsService = new StatsService(accessToken, username);
const overviewStats = await statsService.getOverviewStats();
reply.send(overviewStats);
});
app.get('/activity', async (request, reply) => {
const { accessToken, username } = request.user!;
const statsService = new StatsService(accessToken, username);
const activityStats = await statsService.getActivityStats();
reply.send(activityStats);
});
}

View File

@ -1,40 +1,123 @@
import fastify from 'fastify';
import { authRoutes } from './routes/auth';
import { webhookRoutes } from './routes/webhooks';
import { repoRoutes } from './routes/repos';
import {
serializerCompiler,
validatorCompiler,
ZodTypeProvider
} from 'fastify-type-provider-zod';
import fastifyJwt from '@fastify/jwt';
import fastifyCors from '@fastify/cors';
import 'dotenv/config';
declare module 'fastify' {
interface FastifyRequest {
user?: {
sub: string;
username: string;
avatarUrl: string;
accessToken: string;
};
}
}
import { authRoutes } from './routes/auth';
import { repoRoutes } from './routes/repos';
import { statsRoutes } from './routes/stats';
import { webhookRoutes } from './routes/webhooks';
import swagger from '@fastify/swagger';
// Main function to bootstrap the server
async function bootstrap() {
// Initialize Fastify with ZodTypeProvider
const app = fastify({
logger: true,
});
logger: {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
}).withTypeProvider<ZodTypeProvider>();
app.register(fastifyJwt, {
// Set the validator and serializer for Zod
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
// Register plugins
await app.register(fastifyJwt, {
secret: process.env.JWT_SECRET!,
});
app.register(fastifyCors, {
origin: process.env.FRONTEND_URL,
await app.register(fastifyCors, {
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true,
});
app.register(authRoutes, { prefix: '/auth' });
app.register(webhookRoutes, { prefix: '/webhooks' });
app.register(repoRoutes, { prefix: '/repos' });
// Register Swagger for OpenAPI spec generation
await app.register(swagger, {
openapi: {
info: {
title: 'Project Dash API',
description: 'Backend API documentation for Project Dash application.',
version: '1.0.0',
},
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [
{
bearerAuth: [],
},
],
},
});
await app.listen({ port: 3333, host: '0.0.0.0' });
// --- API Documentation with RapiDoc ---
app.get('/docs', (req, reply) => {
reply.type('text/html').send(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/docs/json"
theme="dark"
render-style="view"
show-header="false"
allow-server-selection="false"
allow-authentication="true"
> </rapi-doc>
</body>
</html>
`);
});
app.get('/docs/json', (req, reply) => {
reply.send(app.swagger());
});
// Register routes
await app.register(authRoutes, { prefix: '/auth' });
await app.register(repoRoutes, { prefix: '/repos' });
await app.register(statsRoutes, { prefix: '/stats' });
await app.register(webhookRoutes, { prefix: '/webhooks' });
// Add a generic error handler
app.setErrorHandler((error, request, reply) => {
app.log.error(error);
reply.status(500).send({ error: 'Internal Server Error' });
});
return app;
}
bootstrap();
// Start the server
(async () => {
try {
const app = await bootstrap();
await app.listen({ port: 3333, host: '0.0.0.0' });
} catch (err) {
console.error(err);
process.exit(1);
}
})();

View File

@ -7,7 +7,8 @@ import { GitHubUser } from '../types/github';
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'];
type ReposListForAuthenticatedUserResponse =
Endpoints['GET /user/repos']['response']['data'];
export async function exchangeCodeForToken(code: string): Promise<string> {
const response = await axios.post(
@ -35,27 +36,31 @@ export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
export class GitHubService {
private octokit: Octokit;
private username: string;
constructor(accessToken: string) {
constructor(accessToken: string, username: string) {
this.octokit = new Octokit({ auth: accessToken });
this.username = username;
}
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
const cacheKey = `repos:${this.username}`;
const cachedRepos = await redis.get(cacheKey);
if (cachedRepos) {
return JSON.parse(cachedRepos as string);
}
const { data } = await this.octokit.repos.listForAuthenticatedUser();
await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }); // Cache for 1 hour
return data;
}
async getRepositoryStats(owner: string, repo: string) {
const cacheKey = `repo-stats:${owner}:${repo}`;
const cachedStats = await redis.get(cacheKey);
if (cachedStats) {
return JSON.parse(cachedStats as string);
}
async getRepositoryStats(owner: string, repo: string, since?: string) {
const [repoData, commits, releases, contributors] = await Promise.all([
this.octokit.repos.get({ owner, repo }),
this.octokit.repos.listCommits({ owner, repo }),
this.octokit.repos.listCommits({ owner, repo, since }),
this.octokit.repos.listReleases({ owner, repo }),
this.octokit.repos.listContributors({ owner, repo }),
]);
@ -69,8 +74,6 @@ export class GitHubService {
contributors: contributors.data.length,
};
await redis.set(cacheKey, JSON.stringify(stats), { ex: 3600 }); // Cache for 1 hour
return stats;
}
}

View File

@ -0,0 +1,42 @@
import { prisma } from '../lib/prisma';
import { GitHubService } from './github.service';
export class StatsService {
private githubService: GitHubService;
constructor(accessToken: string, username: string) {
this.githubService = new GitHubService(accessToken, username);
}
async getOverviewStats() {
const repos = await this.githubService.getRepositories();
const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0);
const totalForks = repos.reduce((acc, repo) => acc + (repo.forks_count || 0), 0);
return {
totalRepos: repos.length,
totalStars,
totalForks,
};
}
async getActivityStats() {
const repos = await this.githubService.getRepositories();
const thirtyDaysAgo = new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000,
).toISOString();
const commitPromises = repos.map((repo) => {
const [owner, repoName] = repo.full_name.split('/');
return this.githubService.getRepositoryStats(owner, repoName, thirtyDaysAgo);
});
const allStats = await Promise.all(commitPromises);
const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0);
return {
totalCommitsLast30Days: totalCommits,
};
}
}

14
src/types/fastify.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import '@fastify/jwt';
declare module '@fastify/jwt' {
interface FastifyJWT {
// This is the type for the payload that is signed
// and the type of the `request.user` object.
user: {
sub: string;
username: string;
avatarUrl: string;
accessToken: string;
};
}
}

View File

@ -1,9 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2020", // More modern target
"module": "commonjs",
"sourceMap": true,
"outDir": "dist",
"esModuleInterop": true
}
"strict": true, // Enable all strict type-checking options
"esModuleInterop": true,
"skipLibCheck": true, // Skip type checking of declaration files
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true
},
"include": [
"src/**/*.ts", // Include all .ts files in the src directory
"src/types/**/*.d.ts" // Include our custom type definitions
],
"exclude": ["node_modules"]
}