feat: add eslint config
This commit is contained in:
parent
434da70b06
commit
5e290945fb
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
.idea/
|
||||||
dist
|
dist
|
||||||
|
|||||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@ -0,0 +1 @@
|
|||||||
|
npx --no -- commitlint --edit $1
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
|||||||
|
npx lint-staged
|
||||||
29
.prettierrc
Normal file
29
.prettierrc
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.prisma",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.json", "*.jsonc"],
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
commitlint.config.js
Normal file
22
commitlint.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
rules: {
|
||||||
|
'type-enum': [
|
||||||
|
2,
|
||||||
|
'always',
|
||||||
|
[
|
||||||
|
'feat', // 新功能
|
||||||
|
'fix', // 修复bug
|
||||||
|
'docs', // 文档变更
|
||||||
|
'style', // 代码格式(不影响代码运行)
|
||||||
|
'refactor', // 重构
|
||||||
|
'perf', // 性能优化
|
||||||
|
'test', // 测试相关
|
||||||
|
'chore', // 构建过程或辅助工具的变动
|
||||||
|
'revert', // 回退
|
||||||
|
'build', // 构建系统或外部依赖项的更改
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'subject-case': [0], // 关闭主题大小写限制
|
||||||
|
},
|
||||||
|
}
|
||||||
168
eslint.config.mjs
Normal file
168
eslint.config.mjs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import eslint from '@eslint/js'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import prettierConfig from 'eslint-config-prettier'
|
||||||
|
import pluginNode from 'eslint-plugin-n'
|
||||||
|
import pluginSecurity from 'eslint-plugin-security'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'**/build/**',
|
||||||
|
'**/.git/**',
|
||||||
|
'**/coverage/**',
|
||||||
|
'**/*.config.js',
|
||||||
|
'**/*.config.mjs',
|
||||||
|
'prisma/migrations/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
eslint.configs.recommended,
|
||||||
|
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.js', '**/*.mjs'],
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint.plugin,
|
||||||
|
n: pluginNode,
|
||||||
|
security: pluginSecurity,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
// Node.js 全局变量
|
||||||
|
console: 'readonly',
|
||||||
|
process: 'readonly',
|
||||||
|
Buffer: 'readonly',
|
||||||
|
__dirname: 'readonly',
|
||||||
|
__filename: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'n/no-process-exit': 'error',
|
||||||
|
'n/no-deprecated-api': 'error',
|
||||||
|
'n/no-unpublished-import': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowModules: [
|
||||||
|
'fastify',
|
||||||
|
'@fastify/autoload',
|
||||||
|
'@fastify/cors',
|
||||||
|
'@fastify/jwt',
|
||||||
|
'@fastify/sensible',
|
||||||
|
'prisma',
|
||||||
|
'@prisma/client',
|
||||||
|
'vitest',
|
||||||
|
'tsx',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'n/prefer-global/buffer': ['error', 'always'],
|
||||||
|
'n/prefer-global/console': ['error', 'always'],
|
||||||
|
'n/prefer-global/process': ['error', 'always'],
|
||||||
|
|
||||||
|
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
||||||
|
'no-await-in-loop': 'warn',
|
||||||
|
'no-return-await': 'off',
|
||||||
|
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/require-await': 'warn',
|
||||||
|
'@typescript-eslint/no-misused-promises': 'error',
|
||||||
|
'@typescript-eslint/await-thenable': 'error',
|
||||||
|
'@typescript-eslint/return-await': ['error', 'in-try-catch'],
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_|^reply|^request',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
prefer: 'type-imports',
|
||||||
|
fixStyle: 'separate-type-imports',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||||
|
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||||
|
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
|
|
||||||
|
'no-throw-literal': 'off',
|
||||||
|
'@typescript-eslint/only-throw-error': 'error',
|
||||||
|
'@typescript-eslint/prefer-promise-reject-errors': 'error',
|
||||||
|
|
||||||
|
'security/detect-object-injection': 'warn',
|
||||||
|
'security/detect-non-literal-regexp': 'warn',
|
||||||
|
'security/detect-unsafe-regex': 'error',
|
||||||
|
'security/detect-buffer-noassert': 'error',
|
||||||
|
'security/detect-eval-with-expression': 'error',
|
||||||
|
'security/detect-non-literal-fs-filename': 'warn',
|
||||||
|
'security/detect-possible-timing-attacks': 'warn',
|
||||||
|
|
||||||
|
'no-nested-ternary': 'warn',
|
||||||
|
'max-depth': ['warn', 4],
|
||||||
|
'max-lines-per-function': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
max: 150,
|
||||||
|
skipBlankLines: true,
|
||||||
|
skipComments: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
complexity: ['warn', 15],
|
||||||
|
|
||||||
|
'sort-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignoreCase: true,
|
||||||
|
ignoreDeclarationSort: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
|
||||||
|
'@typescript-eslint/promise-function-async': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['**/*.test.ts', '**/*.spec.ts', '**/tests/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
|
'max-lines-per-function': 'off',
|
||||||
|
'n/no-unpublished-import': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['*.config.js', '*.config.mjs', '*.config.ts'],
|
||||||
|
rules: {
|
||||||
|
'n/no-unpublished-import': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
prettierConfig
|
||||||
|
)
|
||||||
2620
package-lock.json
generated
2620
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@ -9,7 +9,14 @@
|
|||||||
"build": "prisma generate && tsc",
|
"build": "prisma generate && tsc",
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/server.js",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev"
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"validate": "npm run type-check && npm run lint && npm run format:check",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"author": "Google LLC",
|
"author": "Google LLC",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@ -17,22 +24,54 @@
|
|||||||
"@fastify/cors": "^11.1.0",
|
"@fastify/cors": "^11.1.0",
|
||||||
"@fastify/jwt": "^10.0.0",
|
"@fastify/jwt": "^10.0.0",
|
||||||
"@fastify/swagger": "^9.5.2",
|
"@fastify/swagger": "^9.5.2",
|
||||||
"@octokit/rest": "^19.0.13",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@octokit/types": "^15.0.1",
|
"@octokit/types": "^15.0.1",
|
||||||
"@prisma/client": "^6.18.0",
|
"@prisma/client": "^6.18.0",
|
||||||
"@upstash/redis": "^1.35.6",
|
"@upstash/redis": "^1.35.6",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"fastify": "^5.6.1",
|
"fastify": "^5.6.1",
|
||||||
"fastify-type-provider-zod": "^6.0.0",
|
"fastify-type-provider-zod": "^6.1.0",
|
||||||
|
"prettier-plugin-eslint": "^1.0.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.12.12",
|
"@commitlint/cli": "^20.1.0",
|
||||||
"nodemon": "^3.1.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
|
"@eslint/js": "^9.38.0",
|
||||||
|
"@types/node": "^24.9.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||||
|
"@typescript-eslint/parser": "^8.46.2",
|
||||||
|
"eslint": "^9.38.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-helpers": "^1.2.1",
|
||||||
|
"eslint-plugin-fastify-security-rules": "^1.2.0",
|
||||||
|
"eslint-plugin-n": "^17.23.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"eslint-plugin-security": "^3.0.1",
|
||||||
|
"eslint-plugin-security-node": "^1.1.4",
|
||||||
|
"fastify-tsconfig": "^3.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^16.2.6",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
"pino-pretty": "^13.1.2",
|
"pino-pretty": "^13.1.2",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"prisma": "^6.18.0",
|
"prisma": "^6.18.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.2"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,js,mjs}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"*.{json,md,yml,yaml}": [
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"*.prisma": [
|
||||||
|
"prisma format"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4932
pnpm-lock.yaml
generated
4932
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
28
src/index.ts
28
src/index.ts
@ -1,22 +1,24 @@
|
|||||||
import Fastify from 'fastify';
|
import Fastify from 'fastify'
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: true
|
logger: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
fastify.get('/', async (request, reply) => {
|
fastify.get('/', async () => {
|
||||||
const name = process.env.NAME || 'World';
|
const name = process.env['NAME'] || 'World'
|
||||||
return `Hello ${name}!`;
|
return `Hello ${name}!`
|
||||||
});
|
})
|
||||||
|
|
||||||
const port = parseInt(process.env.PORT || '3000');
|
const port = parseInt(process.env['PORT'] || '3000')
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
await fastify.listen({ port });
|
await fastify.listen({ port })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(err);
|
fastify.log.error(err)
|
||||||
process.exit(1);
|
// eslint-disable-next-line n/no-process-exit
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
start();
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
start().then((r) => console.log(r))
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
// Prevent creating multiple instances of PrismaClient in development
|
const prismaClientSingleton = () => {
|
||||||
// when using hot-reloading (ts-node-dev / nodemon).
|
return new PrismaClient()
|
||||||
const g = global as any;
|
}
|
||||||
|
|
||||||
export const prisma: PrismaClient = g.__prisma ?? new PrismaClient();
|
declare const globalThis: {
|
||||||
|
prismaGlobal: ReturnType<typeof prismaClientSingleton> | undefined
|
||||||
|
} & typeof global
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
|
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||||
|
|
||||||
|
export default prisma
|
||||||
|
|
||||||
|
if (process.env['NODE_ENV'] !== 'production') {
|
||||||
|
globalThis.prismaGlobal = prisma
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Redis } from '@upstash/redis';
|
import { Redis } from '@upstash/redis'
|
||||||
import 'dotenv/config';
|
import 'dotenv/config'
|
||||||
|
|
||||||
export const redis = new Redis({
|
export const redis = new Redis({
|
||||||
url: process.env.UPSTASH_REDIS_URL!,
|
url: process.env.UPSTASH_REDIS_URL,
|
||||||
token: process.env.UPSTASH_REDIS_TOKEN!,
|
token: process.env.UPSTASH_REDIS_TOKEN,
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This middleware function verifies the JWT token from the request.
|
* This middleware function verifies the JWT token from the request.
|
||||||
@ -9,9 +9,10 @@ import { FastifyRequest, FastifyReply } from 'fastify';
|
|||||||
export const authMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
|
export const authMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
|
||||||
try {
|
try {
|
||||||
// This will verify the token and attach the user payload to `req.user`
|
// This will verify the token and attach the user payload to `req.user`
|
||||||
await req.jwtVerify();
|
await req.jwtVerify()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If verification fails, send an unauthorized error
|
// If verification fails, send an unauthorized error
|
||||||
reply.status(401).send({ error: 'Unauthorized' });
|
reply.status(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify'
|
||||||
|
|
||||||
export async function errorMiddleware(app: FastifyInstance) {
|
export async function errorMiddleware(app: FastifyInstance) {
|
||||||
app.setErrorHandler((error, request, reply) => {
|
app.setErrorHandler((error, _, reply) => {
|
||||||
app.log.error(error);
|
app.log.error(error)
|
||||||
reply.status(500).send({ error: 'Internal Server Error' });
|
reply.status(500).send({ error: 'Internal Server Error' })
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
import { FastifyRequest, FastifyReply} from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify'
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
export function verifyGithubWebhook(req: FastifyRequest, reply: FastifyReply) {
|
export function verifyGithubWebhook(req: FastifyRequest, reply: FastifyReply) {
|
||||||
const signature = req.headers['x-hub-signature-256'] as string;
|
const signature = req.headers['x-hub-signature-256'] as string
|
||||||
if (!signature) {
|
if (!signature) {
|
||||||
return reply.status(401).send({ error: 'No signature found' });
|
return reply.status(401).send({ error: 'No signature found' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
|
const secret = process.env['GITHUB_WEBHOOK_SECRET']
|
||||||
const hmac = crypto.createHmac('sha256', secret);
|
const hmac = crypto.createHmac('sha256', secret!)
|
||||||
const digest = `sha256=${hmac.update(JSON.stringify(req.body)).digest('hex')}`;
|
const digest = `sha256=${hmac.update(JSON.stringify(req.body)).digest('hex')}`
|
||||||
|
|
||||||
if (digest !== signature) {
|
if (digest !== signature) {
|
||||||
return reply.status(401).send({ error: 'Invalid signature' });
|
return reply.status(401).send({ error: 'Invalid signature' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod'
|
||||||
import { exchangeCodeForToken, getGithubUser } from '../services/github.service';
|
import { exchangeCodeForToken, getGithubUser } from '@services/github.service'
|
||||||
import { prisma } from '../lib/prisma';
|
import prisma from '@/lib/prisma'
|
||||||
import { createResponseSchema, successResponse, errorResponse, ErrorCode } from '../types/response';
|
import { createResponseSchema, ErrorCode, errorResponse, successResponse } from '@/types/response'
|
||||||
|
|
||||||
// Schema for login request body
|
// Schema for login request body
|
||||||
const loginBodySchema = z.object({
|
const loginBodySchema = z.object({
|
||||||
code: z.string().min(1, 'Authorization code is required'),
|
code: z.string().min(1, 'Authorization code is required'),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Schema for login response data
|
// Schema for login response data
|
||||||
const loginDataSchema = z.object({
|
const loginDataSchema = z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Use the unified response format
|
// Use the unified response format
|
||||||
const loginSuccessSchema = createResponseSchema(loginDataSchema);
|
const loginSuccessSchema = createResponseSchema(loginDataSchema)
|
||||||
const loginErrorSchema = createResponseSchema(z.null());
|
const loginErrorSchema = createResponseSchema(z.null())
|
||||||
|
|
||||||
export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
app.post(
|
app.post(
|
||||||
@ -34,16 +34,16 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
// ✅ Type is automatically inferred from Zod schema
|
// ✅ Type is automatically inferred from Zod schema
|
||||||
const { code } = request.body;
|
const { code } = request.body
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = await exchangeCodeForToken(code);
|
const accessToken = await exchangeCodeForToken(code)
|
||||||
const githubUser = await getGithubUser(accessToken);
|
const githubUser = await getGithubUser(accessToken)
|
||||||
|
|
||||||
// Convert number to string as Prisma schema expects String
|
// Convert number to string as Prisma schema expects String
|
||||||
let user = await prisma.user.findUnique({
|
let user = await prisma.user.findUnique({
|
||||||
where: { githubId: String(githubUser.id) },
|
where: { githubId: String(githubUser.id) },
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await prisma.user.create({
|
user = await prisma.user.create({
|
||||||
@ -52,7 +52,7 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
username: githubUser.login,
|
username: githubUser.login,
|
||||||
avatarUrl: githubUser.avatar_url,
|
avatarUrl: githubUser.avatar_url,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
@ -63,16 +63,14 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
accessToken,
|
accessToken,
|
||||||
},
|
},
|
||||||
{ expiresIn: '1d' },
|
{ expiresIn: '1d' }
|
||||||
);
|
)
|
||||||
|
|
||||||
return successResponse({ token });
|
return successResponse({ token })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
app.log.error({ error }, 'Authentication Error');
|
app.log.error({ error }, 'Authentication Error')
|
||||||
return reply
|
return reply.status(500).send(errorResponse(ErrorCode.AUTH_FAILED, 'Authentication failed'))
|
||||||
.status(500)
|
}
|
||||||
.send(errorResponse(ErrorCode.AUTH_FAILED, 'Authentication failed'));
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod'
|
||||||
import { GitHubService } from '../services/github.service';
|
import { GitHubService } from '@/services/github.service'
|
||||||
import { authMiddleware } from '../middlewares/auth.middleware';
|
import { authMiddleware } from '@/middlewares/auth.middleware'
|
||||||
import { createResponseSchema, successResponse } from '../types/response';
|
import { createResponseSchema, successResponse } from '@/types/response'
|
||||||
|
|
||||||
// Schema for request parameters
|
// Schema for request parameters
|
||||||
const repoParamsSchema = z.object({
|
const repoParamsSchema = z.object({
|
||||||
owner: z.string(),
|
owner: z.string(),
|
||||||
repo: z.string(),
|
repo: z.string(),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Schema for repository statistics response
|
// Schema for repository statistics response
|
||||||
const repoStatsSchema = z.object({
|
const repoStatsSchema = z.object({
|
||||||
@ -18,7 +18,7 @@ const repoStatsSchema = z.object({
|
|||||||
commits: z.number(),
|
commits: z.number(),
|
||||||
releases: z.number(),
|
releases: z.number(),
|
||||||
contributors: z.number(),
|
contributors: z.number(),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Schema for repository list response
|
// Schema for repository list response
|
||||||
const repoListItemSchema = z.object({
|
const repoListItemSchema = z.object({
|
||||||
@ -26,7 +26,7 @@ const repoListItemSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
full_name: z.string(),
|
full_name: z.string(),
|
||||||
private: z.boolean(),
|
private: z.boolean(),
|
||||||
html_url: z.string().url(),
|
html_url: z.url(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
stargazers_count: z.number(),
|
stargazers_count: z.number(),
|
||||||
watchers_count: z.number(),
|
watchers_count: z.number(),
|
||||||
@ -34,19 +34,19 @@ const repoListItemSchema = z.object({
|
|||||||
language: z.string().nullable(),
|
language: z.string().nullable(),
|
||||||
owner: z.object({
|
owner: z.object({
|
||||||
login: z.string(),
|
login: z.string(),
|
||||||
avatar_url: z.string().url(),
|
avatar_url: z.url(),
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
|
|
||||||
const reposListSchema = z.array(repoListItemSchema);
|
const reposListSchema = z.array(repoListItemSchema)
|
||||||
|
|
||||||
// Use the unified response format
|
// Use the unified response format
|
||||||
const reposListResponseSchema = createResponseSchema(reposListSchema);
|
const reposListResponseSchema = createResponseSchema(reposListSchema)
|
||||||
const repoStatsResponseSchema = createResponseSchema(repoStatsSchema);
|
const repoStatsResponseSchema = createResponseSchema(repoStatsSchema)
|
||||||
|
|
||||||
export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
// All routes in this plugin require authentication
|
// All routes in this plugin require authentication
|
||||||
app.addHook('preHandler', authMiddleware);
|
app.addHook('preHandler', authMiddleware)
|
||||||
|
|
||||||
// Route to list repositories for the authenticated user
|
// Route to list repositories for the authenticated user
|
||||||
app.get(
|
app.get(
|
||||||
@ -61,12 +61,12 @@ export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request) => {
|
||||||
const { accessToken, username } = request.user!;
|
const { accessToken, username } = request.user
|
||||||
const githubService = new GitHubService(accessToken, username);
|
const githubService = new GitHubService(accessToken, username)
|
||||||
const repos = await githubService.getRepositories();
|
const repos = await githubService.getRepositories()
|
||||||
return successResponse(repos);
|
return successResponse(repos)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
// Route to get statistics for a specific repository
|
// Route to get statistics for a specific repository
|
||||||
app.get(
|
app.get(
|
||||||
@ -83,11 +83,11 @@ export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
},
|
},
|
||||||
async (request) => {
|
async (request) => {
|
||||||
// ✅ Type is automatically inferred from Zod schema
|
// ✅ Type is automatically inferred from Zod schema
|
||||||
const { owner, repo } = request.params;
|
const { owner, repo } = request.params
|
||||||
const { accessToken, username } = request.user!;
|
const { accessToken, username } = request.user
|
||||||
const githubService = new GitHubService(accessToken, username);
|
const githubService = new GitHubService(accessToken, username)
|
||||||
const stats = await githubService.getRepositoryStats(owner, repo);
|
const stats = await githubService.getRepositoryStats(owner, repo)
|
||||||
return successResponse(stats);
|
return successResponse(stats)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod'
|
||||||
import { StatsService } from '../services/stats.service';
|
import { StatsService } from '@/services/stats.service'
|
||||||
import { authMiddleware } from '../middlewares/auth.middleware';
|
import { authMiddleware } from '@/middlewares/auth.middleware'
|
||||||
import { createResponseSchema, successResponse } from '../types/response';
|
import { createResponseSchema, successResponse } from '@/types/response'
|
||||||
|
|
||||||
// Schema for overview stats response
|
// Schema for overview stats response
|
||||||
const overviewStatsSchema = z.object({
|
const overviewStatsSchema = z.object({
|
||||||
totalRepos: z.number(),
|
totalRepos: z.number(),
|
||||||
totalStars: z.number(),
|
totalStars: z.number(),
|
||||||
totalForks: z.number(),
|
totalForks: z.number(),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Schema for activity stats response
|
// Schema for activity stats response
|
||||||
const activityStatsSchema = z.object({
|
const activityStatsSchema = z.object({
|
||||||
totalCommitsLast30Days: z.number(),
|
totalCommitsLast30Days: z.number(),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Use the unified response format
|
// Use the unified response format
|
||||||
const overviewStatsResponseSchema = createResponseSchema(overviewStatsSchema);
|
const overviewStatsResponseSchema = createResponseSchema(overviewStatsSchema)
|
||||||
const activityStatsResponseSchema = createResponseSchema(activityStatsSchema);
|
const activityStatsResponseSchema = createResponseSchema(activityStatsSchema)
|
||||||
|
|
||||||
export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
app.addHook('preHandler', authMiddleware);
|
app.addHook('preHandler', authMiddleware)
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/overview',
|
'/overview',
|
||||||
@ -35,12 +35,12 @@ export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request) => {
|
||||||
const { accessToken, username } = request.user!;
|
const { accessToken, username } = request.user
|
||||||
const statsService = new StatsService(accessToken, username);
|
const statsService = new StatsService(accessToken, username)
|
||||||
const overviewStats = await statsService.getOverviewStats();
|
const overviewStats = await statsService.getOverviewStats()
|
||||||
return successResponse(overviewStats);
|
return successResponse(overviewStats)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/activity',
|
'/activity',
|
||||||
@ -54,10 +54,10 @@ export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request) => {
|
||||||
const { accessToken, username } = request.user!;
|
const { accessToken, username } = request.user
|
||||||
const statsService = new StatsService(accessToken, username);
|
const statsService = new StatsService(accessToken, username)
|
||||||
const activityStats = await statsService.getActivityStats();
|
const activityStats = await statsService.getActivityStats()
|
||||||
return successResponse(activityStats);
|
return successResponse(activityStats)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
|
||||||
import { z } from 'zod';
|
import { z } from 'zod'
|
||||||
import { verifyGithubWebhook } from '../middlewares/verify-webhook.middleware';
|
import { verifyGithubWebhook } from '@/middlewares/verify-webhook.middleware'
|
||||||
import { handleWebhook } from '../services/webhook.service';
|
import { handleWebhook } from '@/services/webhook.service'
|
||||||
import { GitHubWebhookPayload } from '../types/github';
|
import type { GitHubWebhookPayload } from '@/types/github'
|
||||||
import { createResponseSchema, successResponse, errorResponse, ErrorCode } from '../types/response';
|
import { createResponseSchema, ErrorCode, errorResponse, successResponse } from '@/types/response'
|
||||||
|
|
||||||
// Schema for webhook payload (flexible since payloads vary by event type)
|
// Schema for webhook payload (flexible since payloads vary by event type)
|
||||||
const webhookBodySchema = z.record(z.string(), z.any());
|
const webhookBodySchema = z.record(z.string(), z.any())
|
||||||
|
|
||||||
// Schema for webhook response data
|
// Schema for webhook response data
|
||||||
const webhookDataSchema = z.object({
|
const webhookDataSchema = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
})
|
||||||
|
|
||||||
// Use the unified response format
|
// Use the unified response format
|
||||||
const webhookSuccessSchema = createResponseSchema(webhookDataSchema);
|
const webhookSuccessSchema = createResponseSchema(webhookDataSchema)
|
||||||
const webhookErrorSchema = createResponseSchema(z.null());
|
const webhookErrorSchema = createResponseSchema(z.null())
|
||||||
|
|
||||||
export const webhookRoutes: FastifyPluginAsyncZod = async (app) => {
|
export const webhookRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
app.post(
|
app.post(
|
||||||
'/github',
|
'/github',
|
||||||
{
|
{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
preHandler: verifyGithubWebhook,
|
preHandler: verifyGithubWebhook,
|
||||||
schema: {
|
schema: {
|
||||||
description: 'Handle GitHub webhook events',
|
description: 'Handle GitHub webhook events',
|
||||||
@ -35,14 +36,14 @@ export const webhookRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
// Type cast to GitHubWebhookPayload for service function
|
// Type cast to GitHubWebhookPayload for service function
|
||||||
await handleWebhook(request.body as GitHubWebhookPayload);
|
await handleWebhook(request.body as GitHubWebhookPayload)
|
||||||
return successResponse({ message: 'Webhook received' });
|
return successResponse({ message: 'Webhook received' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
app.log.error({ error }, 'Webhook processing error');
|
app.log.error({ error }, 'Webhook processing error')
|
||||||
return reply
|
return reply
|
||||||
.status(500)
|
.status(500)
|
||||||
.send(errorResponse(ErrorCode.INTERNAL_ERROR, 'Webhook processing failed'));
|
.send(errorResponse(ErrorCode.INTERNAL_ERROR, 'Webhook processing failed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
100
src/server.ts
100
src/server.ts
@ -1,19 +1,16 @@
|
|||||||
import fastify from 'fastify';
|
import fastify from 'fastify'
|
||||||
import {
|
import type { ZodTypeProvider } from 'fastify-type-provider-zod'
|
||||||
serializerCompiler,
|
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'
|
||||||
validatorCompiler,
|
import fastifyJwt from '@fastify/jwt'
|
||||||
ZodTypeProvider
|
import fastifyCors from '@fastify/cors'
|
||||||
} from 'fastify-type-provider-zod';
|
import 'dotenv/config'
|
||||||
import fastifyJwt from '@fastify/jwt';
|
|
||||||
import fastifyCors from '@fastify/cors';
|
|
||||||
import 'dotenv/config';
|
|
||||||
|
|
||||||
import { authRoutes } from './routes/auth';
|
import { authRoutes } from '@/routes/auth'
|
||||||
import { repoRoutes } from './routes/repos';
|
import { repoRoutes } from '@/routes/repos'
|
||||||
import { statsRoutes } from './routes/stats';
|
import { statsRoutes } from '@/routes/stats'
|
||||||
import { webhookRoutes } from './routes/webhooks';
|
import { webhookRoutes } from '@/routes/webhooks'
|
||||||
|
|
||||||
import swagger from '@fastify/swagger';
|
import swagger from '@fastify/swagger'
|
||||||
|
|
||||||
// Main function to bootstrap the server
|
// Main function to bootstrap the server
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
@ -28,21 +25,21 @@ async function bootstrap() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).withTypeProvider<ZodTypeProvider>();
|
}).withTypeProvider<ZodTypeProvider>()
|
||||||
|
|
||||||
// Set the validator and serializer for Zod
|
// Set the validator and serializer for Zod
|
||||||
app.setValidatorCompiler(validatorCompiler);
|
app.setValidatorCompiler(validatorCompiler)
|
||||||
app.setSerializerCompiler(serializerCompiler);
|
app.setSerializerCompiler(serializerCompiler)
|
||||||
|
|
||||||
// Register plugins
|
// Register plugins
|
||||||
await app.register(fastifyJwt, {
|
await app.register(fastifyJwt, {
|
||||||
secret: process.env.JWT_SECRET!,
|
secret: process.env.JWT_SECRET,
|
||||||
});
|
})
|
||||||
|
|
||||||
await app.register(fastifyCors, {
|
await app.register(fastifyCors, {
|
||||||
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Register Swagger for OpenAPI spec generation
|
// Register Swagger for OpenAPI spec generation
|
||||||
await app.register(swagger, {
|
await app.register(swagger, {
|
||||||
@ -67,11 +64,11 @@ async function bootstrap() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// --- Docs portal ---
|
// --- Docs portal ---
|
||||||
// Top-level docs landing page that links to multiple renderers (ReDoc, Stoplight Elements, RapiDoc)
|
// Top-level docs landing page that links to multiple renderers (ReDoc, Stoplight Elements, RapiDoc)
|
||||||
app.get('/docs', (req, reply) => {
|
app.get('/docs', (_, reply) => {
|
||||||
reply.type('text/html').send(`
|
reply.type('text/html').send(`
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
@ -118,11 +115,11 @@ async function bootstrap() {
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`)
|
||||||
});
|
})
|
||||||
|
|
||||||
// --- RapiDoc route (moved from /docs to /docs/rapidoc) ---
|
// --- RapiDoc route (moved from /docs to /docs/rapidoc) ---
|
||||||
app.get('/docs/rapidoc', (req, reply) => {
|
app.get('/docs/rapidoc', (_, reply) => {
|
||||||
reply.type('text/html').send(`
|
reply.type('text/html').send(`
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
@ -141,12 +138,12 @@ async function bootstrap() {
|
|||||||
> </rapi-doc>
|
> </rapi-doc>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`)
|
||||||
});
|
})
|
||||||
|
|
||||||
// --- API Documentation with ReDoc (alternative nicer UI) ---
|
// --- API Documentation with ReDoc (alternative nicer UI) ---
|
||||||
// ReDoc gives a clean, modern single-page OpenAPI rendering similar to Knife4j's look.
|
// ReDoc gives a clean, modern single-page OpenAPI rendering similar to Knife4j's look.
|
||||||
app.get('/docs/redoc', (req, reply) => {
|
app.get('/docs/redoc', (_, reply) => {
|
||||||
reply.type('text/html').send(`
|
reply.type('text/html').send(`
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
@ -175,12 +172,12 @@ async function bootstrap() {
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`)
|
||||||
});
|
})
|
||||||
|
|
||||||
// --- Stoplight Elements (interactive, Knife4j-like feel) ---
|
// --- Stoplight Elements (interactive, Knife4j-like feel) ---
|
||||||
// Uses Stoplight Elements Web Components from CDN to render a modern interactive portal.
|
// Uses Stoplight Elements Web Components from CDN to render a modern interactive portal.
|
||||||
app.get('/docs/stoplight', (req, reply) => {
|
app.get('/docs/stoplight', (_, reply) => {
|
||||||
reply.type('text/html').send(`
|
reply.type('text/html').send(`
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
@ -227,35 +224,36 @@ async function bootstrap() {
|
|||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`)
|
||||||
});
|
})
|
||||||
|
|
||||||
app.get('/docs/json', (req, reply) => {
|
app.get('/docs/json', (_, reply) => {
|
||||||
reply.send(app.swagger());
|
reply.send(app.swagger())
|
||||||
});
|
})
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
await app.register(authRoutes, { prefix: '/auth' });
|
await app.register(authRoutes, { prefix: '/auth' })
|
||||||
await app.register(repoRoutes, { prefix: '/repos' });
|
await app.register(repoRoutes, { prefix: '/repos' })
|
||||||
await app.register(statsRoutes, { prefix: '/stats' });
|
await app.register(statsRoutes, { prefix: '/stats' })
|
||||||
await app.register(webhookRoutes, { prefix: '/webhooks' });
|
await app.register(webhookRoutes, { prefix: '/webhooks' })
|
||||||
|
|
||||||
// Add a generic error handler
|
// Add a generic error handler
|
||||||
app.setErrorHandler((error, request, reply) => {
|
app.setErrorHandler((error, _, reply) => {
|
||||||
app.log.error(error);
|
app.log.error(error)
|
||||||
reply.status(500).send({ error: 'Internal Server Error' });
|
reply.status(500).send({ error: 'Internal Server Error' })
|
||||||
});
|
})
|
||||||
|
|
||||||
return app;
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
(async () => {
|
await (async () => {
|
||||||
try {
|
try {
|
||||||
const app = await bootstrap();
|
const app = await bootstrap()
|
||||||
await app.listen({ port: 3333, host: '0.0.0.0' });
|
await app.listen({ port: 3333, host: '0.0.0.0' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
process.exit(1);
|
// eslint-disable-next-line n/no-process-exit
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})();
|
})()
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
import { Redis } from '@upstash/redis';
|
import { Redis } from '@upstash/redis'
|
||||||
|
|
||||||
export const redis = new Redis({
|
export const redis = new Redis({
|
||||||
url: process.env.UPSTASH_REDIS_URL!,
|
url: process.env.UPSTASH_REDIS_URL,
|
||||||
token: process.env.UPSTASH_REDIS_TOKEN!,
|
token: process.env.UPSTASH_REDIS_TOKEN,
|
||||||
});
|
})
|
||||||
|
|
||||||
export class CacheService {
|
export class CacheService {
|
||||||
async get(key: string) {
|
async get(key: string) {
|
||||||
return await redis.get(key);
|
return redis.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: any, ttl?: number) {
|
async set(key: string, value: any, ttl?: number) {
|
||||||
// Only pass ex option if ttl is defined
|
// Only pass ex option if ttl is defined
|
||||||
if (ttl !== undefined) {
|
if (ttl !== undefined) {
|
||||||
return await redis.set(key, value, { ex: ttl });
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return redis.set(key, value, { ex: ttl })
|
||||||
}
|
}
|
||||||
return await redis.set(key, value);
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return redis.set(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest'
|
||||||
import { Endpoints } from '@octokit/types';
|
import type { Endpoints } from '@octokit/types'
|
||||||
import axios from 'axios';
|
import axios from 'axios'
|
||||||
import { redis } from '../lib/redis';
|
import { redis } from '@/lib/redis'
|
||||||
import { GitHubUser } from '../types/github';
|
import type { GitHubUser } from '@/types/github'
|
||||||
|
|
||||||
const GITHUB_API_BASE_URL = 'https://api.github.com';
|
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 =
|
type ReposListForAuthenticatedUserResponse = Endpoints['GET /user/repos']['response']['data']
|
||||||
Endpoints['GET /user/repos']['response']['data'];
|
|
||||||
|
|
||||||
export async function exchangeCodeForToken(code: string): Promise<string> {
|
export async function exchangeCodeForToken(code: string): Promise<string> {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
@ -20,41 +19,43 @@ export async function exchangeCodeForToken(code: string): Promise<string> {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
return response.data.access_token;
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access
|
||||||
|
return response.data?.access_token
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
|
export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
|
||||||
const response = await axios.get<GitHubUser>(`${GITHUB_API_BASE_URL}/user`, {
|
const response = await axios.get<GitHubUser>(`${GITHUB_API_BASE_URL}/user`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
});
|
})
|
||||||
|
|
||||||
return response.data;
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GitHubService {
|
export class GitHubService {
|
||||||
private octokit: Octokit;
|
private octokit: Octokit
|
||||||
private username: string;
|
private username: string
|
||||||
|
|
||||||
constructor(accessToken: string, username: string) {
|
constructor(accessToken: string, username: string) {
|
||||||
this.octokit = new Octokit({ auth: accessToken });
|
this.octokit = new Octokit({ auth: accessToken })
|
||||||
this.username = username;
|
this.username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
||||||
const cacheKey = `repos:${this.username}`;
|
const cacheKey = `repos:${this.username}`
|
||||||
const cachedRepos = await redis.get(cacheKey);
|
const cachedRepos = await redis.get(cacheKey)
|
||||||
|
|
||||||
if (cachedRepos) {
|
if (cachedRepos) {
|
||||||
return JSON.parse(cachedRepos as string);
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return JSON.parse(cachedRepos as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await this.octokit.repos.listForAuthenticatedUser();
|
const { data } = await this.octokit.repos.listForAuthenticatedUser()
|
||||||
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
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRepositoryStats(owner: string, repo: string, since?: string) {
|
async getRepositoryStats(owner: string, repo: string, since?: string) {
|
||||||
@ -63,7 +64,7 @@ export class GitHubService {
|
|||||||
this.octokit.repos.listCommits({ owner, repo, since }),
|
this.octokit.repos.listCommits({ owner, repo, since }),
|
||||||
this.octokit.repos.listReleases({ owner, repo }),
|
this.octokit.repos.listReleases({ owner, repo }),
|
||||||
this.octokit.repos.listContributors({ owner, repo }),
|
this.octokit.repos.listContributors({ owner, repo }),
|
||||||
]);
|
])
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
stars: repoData.data.stargazers_count,
|
stars: repoData.data.stargazers_count,
|
||||||
@ -72,8 +73,8 @@ export class GitHubService {
|
|||||||
commits: commits.data.length,
|
commits: commits.data.length,
|
||||||
releases: releases.data.length,
|
releases: releases.data.length,
|
||||||
contributors: contributors.data.length,
|
contributors: contributors.data.length,
|
||||||
};
|
}
|
||||||
|
|
||||||
return stats;
|
return stats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,42 @@
|
|||||||
import { prisma } from '../lib/prisma';
|
import { GitHubService } from './github.service'
|
||||||
import { GitHubService } from './github.service';
|
|
||||||
|
|
||||||
export class StatsService {
|
export class StatsService {
|
||||||
private githubService: GitHubService;
|
private githubService: GitHubService
|
||||||
|
|
||||||
constructor(accessToken: string, username: string) {
|
constructor(accessToken: string, username: string) {
|
||||||
this.githubService = new GitHubService(accessToken, username);
|
this.githubService = new GitHubService(accessToken, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOverviewStats() {
|
async getOverviewStats() {
|
||||||
const repos = await this.githubService.getRepositories();
|
const repos = await this.githubService.getRepositories()
|
||||||
const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0);
|
const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0)
|
||||||
const totalForks = repos.reduce((acc, repo) => acc + (repo.forks_count || 0), 0);
|
const totalForks = repos.reduce((acc, repo) => acc + (repo.forks_count || 0), 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalRepos: repos.length,
|
totalRepos: repos.length,
|
||||||
totalStars,
|
totalStars,
|
||||||
totalForks,
|
totalForks,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActivityStats() {
|
async getActivityStats() {
|
||||||
const repos = await this.githubService.getRepositories();
|
const repos = await this.githubService.getRepositories()
|
||||||
const thirtyDaysAgo = new Date(
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
||||||
).toISOString();
|
|
||||||
|
|
||||||
const commitPromises = repos.map((repo) => {
|
const commitPromises = repos.map(async (repo) => {
|
||||||
const [owner, repoName] = repo.full_name.split('/');
|
const [owner, repoName] = repo.full_name.split('/')
|
||||||
return this.githubService.getRepositoryStats(owner, repoName, thirtyDaysAgo);
|
if (!owner || !repoName) {
|
||||||
});
|
return { commits: 0 }
|
||||||
|
}
|
||||||
|
return this.githubService.getRepositoryStats(owner, repoName, thirtyDaysAgo)
|
||||||
|
})
|
||||||
|
|
||||||
const allStats = await Promise.all(commitPromises);
|
const allStats = await Promise.all(commitPromises)
|
||||||
|
|
||||||
const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0);
|
const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalCommitsLast30Days: totalCommits,
|
totalCommitsLast30Days: totalCommits,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { prisma } from '../lib/prisma';
|
import prisma from '@/lib/prisma'
|
||||||
import { GitHubWebhookPayload } from '../types/github';
|
import type { GitHubWebhookPayload } from '@/types/github'
|
||||||
|
|
||||||
export async function handleWebhook(payload: GitHubWebhookPayload) {
|
export async function handleWebhook(payload: GitHubWebhookPayload) {
|
||||||
const { action, pull_request, repository } = payload;
|
const { action, pull_request, repository } = payload
|
||||||
|
|
||||||
if (action === 'opened' || action === 'closed') {
|
if (action === 'opened' || action === 'closed') {
|
||||||
const { number, title, state, user, created_at, closed_at } = pull_request;
|
const { number, title, state, user, created_at, closed_at } = pull_request
|
||||||
|
|
||||||
await prisma.pullRequest.upsert({
|
await prisma.pullRequest.upsert({
|
||||||
where: { repositoryId_number: { repositoryId: repository.id, number } },
|
where: { repositoryId_number: { repositoryId: repository.id, number } },
|
||||||
update: {
|
update: {
|
||||||
state,
|
state,
|
||||||
closedAt: closed_at ? new Date(closed_at) : null
|
closedAt: closed_at ? new Date(closed_at) : null,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
number,
|
number,
|
||||||
@ -40,6 +40,6 @@ export async function handleWebhook(payload: GitHubWebhookPayload) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,35 @@
|
|||||||
* Represents the user data structure returned from the GitHub API.
|
* Represents the user data structure returned from the GitHub API.
|
||||||
*/
|
*/
|
||||||
export interface GitHubUser {
|
export interface GitHubUser {
|
||||||
id: number;
|
id: number
|
||||||
login: string;
|
login: string
|
||||||
name: string | null;
|
name: string | null
|
||||||
avatar_url: string;
|
avatar_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic type for the GitHub webhook payload.
|
* A generic type for the GitHub webhook payload.
|
||||||
* The actual structure varies widely depending on the event type.
|
* The actual structure varies widely depending on the event type.
|
||||||
*/
|
*/
|
||||||
export type GitHubWebhookPayload = Record<string, any>;
|
export interface GitHubWebhookPayload {
|
||||||
|
action: string
|
||||||
|
pull_request: {
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
state: string
|
||||||
|
user: {
|
||||||
|
id: number
|
||||||
|
login: string
|
||||||
|
avatar_url: string
|
||||||
|
}
|
||||||
|
created_at: string
|
||||||
|
closed_at: string | null
|
||||||
|
}
|
||||||
|
repository: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
owner: {
|
||||||
|
login: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard API Response Format
|
* Standard API Response Format
|
||||||
@ -8,12 +8,12 @@ import { z } from 'zod';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Generic response schema creator
|
// Generic response schema creator
|
||||||
export function createResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
export function createResponseSchema<T extends z.ZodType>(dataSchema: T) {
|
||||||
return z.object({
|
return z.object({
|
||||||
code: z.number(),
|
code: z.number(),
|
||||||
msg: z.string(),
|
msg: z.string(),
|
||||||
data: dataSchema,
|
data: dataSchema,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success response with data
|
// Success response with data
|
||||||
@ -22,7 +22,7 @@ export function successResponse<T>(data: T) {
|
|||||||
code: 0,
|
code: 0,
|
||||||
msg: '',
|
msg: '',
|
||||||
data,
|
data,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error response
|
// Error response
|
||||||
@ -31,7 +31,7 @@ export function errorResponse(code: number, msg: string) {
|
|||||||
code,
|
code,
|
||||||
msg,
|
msg,
|
||||||
data: null,
|
data: null,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common error codes
|
// Common error codes
|
||||||
@ -45,11 +45,11 @@ export const ErrorCode = {
|
|||||||
AUTH_FAILED: 1001,
|
AUTH_FAILED: 1001,
|
||||||
GITHUB_API_ERROR: 1002,
|
GITHUB_API_ERROR: 1002,
|
||||||
WEBHOOK_VERIFICATION_FAILED: 1003,
|
WEBHOOK_VERIFICATION_FAILED: 1003,
|
||||||
} as const;
|
} as const
|
||||||
|
|
||||||
// Error response schema
|
// Error response schema
|
||||||
export const errorResponseSchema = z.object({
|
export const errorResponseSchema = z.object({
|
||||||
code: z.number(),
|
code: z.number(),
|
||||||
msg: z.string(),
|
msg: z.string(),
|
||||||
data: z.null(),
|
data: z.null(),
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,18 +1,58 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "fastify-tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020", // More modern target
|
|
||||||
"module": "commonjs",
|
|
||||||
"strict": true, // Enable all strict type-checking options
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true, // Skip type checking of declaration files
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"sourceMap": true
|
"declaration": true,
|
||||||
},
|
"declarationMap": true,
|
||||||
"include": [
|
"sourceMap": true,
|
||||||
"src/**/*.ts", // Include all .ts files in the src directory
|
|
||||||
"src/types/**/*.d.ts" // Include our custom type definitions
|
"module": "NodeNext",
|
||||||
],
|
"moduleResolution": "NodeNext",
|
||||||
"exclude": ["node_modules"]
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": false,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@config/*": ["src/config/*"],
|
||||||
|
"@routes/*": ["src/routes/*"],
|
||||||
|
"@services/*": ["src/services/*"],
|
||||||
|
"@utils/*": ["src/utils/*"],
|
||||||
|
"@types/*": ["src/types/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "node_modules/@prisma/client"],
|
||||||
|
"exclude": ["node_modules", "dist", "build", "coverage", "**/*.test.ts", "**/*.spec.ts"],
|
||||||
|
"ts-node": {
|
||||||
|
"require": ["tsconfig-paths/register"],
|
||||||
|
"transpileOnly": true,
|
||||||
|
"files": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user