├── .env.example ├── next.config.js ├── src ├── lib │ ├── verifySignature.ts │ └── followUser.ts └── app │ └── api │ └── webhook │ └── route.ts ├── package.json ├── .gitignore ├── tsconfig.json ├── LICENSE └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | # Rename this file to .env.local and fill in your secrets 2 | GITHUB_TOKEN=ghp_xxxyourtoken 3 | GITHUB_WEBHOOK_SECRET=your_webhook_secret 4 | GIST_ID=your_gist_id 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /src/lib/verifySignature.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | export function verifySignature(secret: string, payload: string, signature: string | null) { 4 | if (!signature) return false 5 | const hmac = crypto.createHmac('sha256', secret) 6 | const digest = `sha256=${hmac.update(payload).digest('hex')}` 7 | return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature)) 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/followUser.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest' 2 | 3 | const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) 4 | 5 | export async function followUser(username: string) { 6 | await octokit.request('PUT /user/following/{username}', { username }) 7 | } 8 | 9 | export async function unfollowUser(username: string) { 10 | await octokit.request('DELETE /user/following/{username}', { username }) 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thanks-for-the-star-nextjs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@octokit/rest": "^20.0.2", 12 | "next": "^14.2.28", 13 | "react": "18.2.0", 14 | "react-dom": "18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.15.18", 18 | "@types/react": "^19.1.4", 19 | "typescript": "^5.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | package-lock.json 8 | yarn.lock 9 | pnpm-lock.yaml 10 | 11 | # Next.js 12 | .next/ 13 | out/ 14 | .vercel/ 15 | 16 | # Environment variables 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Logs 24 | logs/ 25 | *.log 26 | 27 | # OS 28 | .DS_Store 29 | Thumbs.db 30 | 31 | # Editor directories and files 32 | .vscode/ 33 | .idea/ 34 | *.sublime-workspace 35 | *.sublime-project 36 | *.swp 37 | *.swo 38 | *.bak 39 | *.tmp -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "baseUrl": "src", 17 | "paths": { 18 | "@/*": ["*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Ragib Al Asad 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /src/app/api/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { verifySignature } from '@/lib/verifySignature' 3 | import { followUser, unfollowUser } from '@/lib/followUser' 4 | 5 | export async function POST(req: NextRequest) { 6 | const body = await req.text() 7 | const signature = req.headers.get('x-hub-signature-256') 8 | const secret = process.env.GITHUB_WEBHOOK_SECRET! 9 | 10 | if (!verifySignature(secret, body, signature)) { 11 | console.warn('Invalid signature') 12 | return NextResponse.json({ message: 'Invalid signature' }, { status: 401 }) 13 | } 14 | 15 | const githubEvent = req.headers.get('x-github-event') 16 | if (githubEvent !== 'star') { 17 | console.log('Not a star event') 18 | return NextResponse.json({ message: 'Ignored non-star event' }) 19 | } 20 | 21 | const event = JSON.parse(body) 22 | const username = event.sender?.login 23 | 24 | if (!['created', 'deleted'].includes(event.action) || !username) { 25 | console.warn('Irrelevant action or missing username') 26 | return NextResponse.json({ message: 'Ignored' }) 27 | } 28 | 29 | try { 30 | console.log(`Received event: ${event.action} from ${username}`) 31 | 32 | if (event.action === 'created') { 33 | await followUser(username) 34 | console.log(`Followed user: ${username}`) 35 | } else if (event.action === 'deleted') { 36 | await unfollowUser(username) 37 | console.log(`Unfollowed user: ${username}`) 38 | } 39 | 40 | return NextResponse.json({ message: 'Success' }) 41 | 42 | } catch (err: any) { 43 | console.error('Error:', err.message || err) 44 | return NextResponse.json({ error: 'Internal error' }, { status: 500 }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
9 | Automatically follow people who star your repo ✨
10 | Give back with a follow, powered by Next.js & GitHub Webhook
11 |
12 |