Skip to content

Commit daafbcc

Browse files
committed
feat: Upgrade to Freemium B2B/B2E platform with Stripe, NextAuth, Supabase
- Add authentication: NextAuth.js with Google OAuth & credentials - Add payment: Stripe subscriptions (FREE/PRO/BUSINESS/ENTERPRISE) - Add database: Prisma schema with 30+ models for Supabase - Add features: Scan history, Watchlist, API keys, PDF export - Add B2B: Organization dashboard, member management - Add pages: Dashboard, Pricing, Billing, Transparency, API Keys - Add middleware: Auth protection & tier-based access control - Add rate limiting: Tier-based quota system - Fix all TypeScript errors (0 errors) - Add documentation: Setup guides for Supabase, Stripe, deployment
1 parent 8bfbdcc commit daafbcc

43 files changed

Lines changed: 8600 additions & 103 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
# Database (Supabase/PostgreSQL)
2-
DATABASE_URL="postgresql://user:password@host:6543/database?pgbouncer=true"
3-
DIRECT_URL="postgresql://user:password@host:5432/database"
1+
# ==============================================
2+
# DATABASE - SUPABASE
3+
# ==============================================
4+
# Get from: https://supabase.com/dashboard/project/_/settings/database
5+
# Transaction mode (for Prisma Migrate) - Port 5432
6+
DIRECT_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres"
47

8+
# Session mode (for queries via PgBouncer) - Port 6543
9+
DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
10+
11+
# ==============================================
12+
# AI MODELS
13+
# ==============================================
514
# HuggingFace API (with Inference Provider permission)
615
# Get from: https://huggingface.co/settings/tokens
716
HUGGINGFACE_API_KEY="hf_xxxxxxxxxxxxxxxxxxxx"
@@ -10,11 +19,44 @@ HUGGINGFACE_API_KEY="hf_xxxxxxxxxxxxxxxxxxxx"
1019
# Get from: https://console.groq.com/keys
1120
GROQ_API_KEY="gsk_xxxxxxxxxxxxxxxxxxxx"
1221

22+
# ==============================================
23+
# EXTERNAL SERVICES
24+
# ==============================================
1325
# VirusTotal API (optional, for malware/phishing detection)
1426
# Get from: https://www.virustotal.com/gui/my-apikey
1527
# Free tier: 500 requests/day, 4 requests/minute
1628
VIRUSTOTAL_API_KEY="your_virustotal_api_key_here"
1729

30+
# ==============================================
31+
# AUTHENTICATION (NextAuth.js)
32+
# ==============================================
33+
NEXTAUTH_URL="http://localhost:3000"
34+
NEXTAUTH_SECRET="your-nextauth-secret-here-generate-with-openssl-rand-base64-32"
35+
36+
# OAuth Providers (Optional)
37+
GOOGLE_CLIENT_ID="your-google-client-id"
38+
GOOGLE_CLIENT_SECRET="your-google-client-secret"
39+
40+
# ==============================================
41+
# STRIPE PAYMENT
42+
# ==============================================
43+
STRIPE_SECRET_KEY="sk_test_xxxxx"
44+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_xxxxx"
45+
STRIPE_WEBHOOK_SECRET="whsec_xxxxx"
46+
47+
# Price IDs (create these in Stripe Dashboard)
48+
NEXT_PUBLIC_STRIPE_PRO_PRICE_ID="price_xxxxx"
49+
NEXT_PUBLIC_STRIPE_BUSINESS_PRICE_ID="price_xxxxx"
50+
51+
# ==============================================
52+
# APP CONFIGURATION
53+
# ==============================================
54+
NEXT_PUBLIC_APP_URL="http://localhost:3000"
55+
NEXT_PUBLIC_APP_NAME="ANTI-SCAM"
56+
57+
# ==============================================
58+
# ADMIN PANEL
59+
# ==============================================
1860
# Admin Secret (generate a strong random string)
1961
# Use: openssl rand -hex 32
2062
ADMIN_SECRET="12345"

.github/workflows/deploy.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main # Hoặc master, tùy theo tên branch chính của bạn
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: "pages"
16+
cancel-in-progress: false
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: Setup Node
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: '20'
30+
cache: 'npm'
31+
32+
- name: Install dependencies
33+
run: npm ci
34+
35+
- name: Setup Pages
36+
uses: actions/configure-pages@v4
37+
38+
- name: Build with Next.js
39+
run: npm run export
40+
env:
41+
# Thêm các environment variables cần thiết
42+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
43+
HUGGINGFACE_API_KEY: ${{ secrets.HUGGINGFACE_API_KEY }}
44+
VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}
45+
ADMIN_SECRET: ${{ secrets.ADMIN_SECRET }}
46+
47+
- name: Upload artifact
48+
uses: actions/upload-pages-artifact@v3
49+
with:
50+
path: ./out
51+
52+
deploy:
53+
environment:
54+
name: github-pages
55+
url: ${{ steps.deployment.outputs.page_url }}
56+
runs-on: ubuntu-latest
57+
needs: build
58+
steps:
59+
- name: Deploy to GitHub Pages
60+
id: deployment
61+
uses: actions/deploy-pages@v4

app/api/api-keys/[id]/route.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Delete API Key
3+
*/
4+
5+
import { NextRequest, NextResponse } from 'next/server'
6+
import { getServerSession } from 'next-auth'
7+
import { authOptions } from '@/app/lib/auth'
8+
import prisma from '@/app/lib/db'
9+
10+
export async function DELETE(
11+
request: NextRequest,
12+
{ params }: { params: { id: string } }
13+
) {
14+
try {
15+
const session = await getServerSession(authOptions)
16+
17+
if (!session?.user) {
18+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
19+
}
20+
21+
const { id } = params
22+
23+
const key = await prisma.apiKey.findUnique({
24+
where: { id },
25+
})
26+
27+
if (!key || key.userId !== session.user.id) {
28+
return NextResponse.json(
29+
{ error: 'Not found or unauthorized' },
30+
{ status: 404 }
31+
)
32+
}
33+
34+
await prisma.apiKey.delete({
35+
where: { id },
36+
})
37+
38+
return NextResponse.json({ success: true })
39+
40+
} catch (error) {
41+
console.error('Delete API key error:', error)
42+
return NextResponse.json(
43+
{ error: 'Failed to delete' },
44+
{ status: 500 }
45+
)
46+
}
47+
}

app/api/api-keys/route.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* API Keys Management API
3+
*/
4+
5+
import { NextRequest, NextResponse } from 'next/server'
6+
import { getServerSession } from 'next-auth'
7+
import { authOptions } from '@/app/lib/auth'
8+
import prisma from '@/app/lib/db'
9+
import { nanoid } from 'nanoid'
10+
import { hash } from 'bcryptjs'
11+
12+
export async function GET(request: NextRequest) {
13+
try {
14+
const session = await getServerSession(authOptions)
15+
16+
if (!session?.user) {
17+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
if (session.user.tier === 'FREE' || session.user.tier === 'PRO') {
21+
return NextResponse.json(
22+
{ error: 'API access chỉ dành cho gói Business trở lên' },
23+
{ status: 403 }
24+
)
25+
}
26+
27+
const apiKeys = await prisma.apiKey.findMany({
28+
where: { userId: session.user.id },
29+
select: {
30+
id: true,
31+
name: true,
32+
prefix: true,
33+
lastUsedAt: true,
34+
createdAt: true,
35+
isActive: true,
36+
},
37+
orderBy: { createdAt: 'desc' },
38+
})
39+
40+
return NextResponse.json({ success: true, data: apiKeys })
41+
42+
} catch (error) {
43+
console.error('API keys fetch error:', error)
44+
return NextResponse.json(
45+
{ error: 'Failed to fetch API keys' },
46+
{ status: 500 }
47+
)
48+
}
49+
}
50+
51+
export async function POST(request: NextRequest) {
52+
try {
53+
const session = await getServerSession(authOptions)
54+
55+
if (!session?.user) {
56+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
57+
}
58+
59+
if (session.user.tier === 'FREE' || session.user.tier === 'PRO') {
60+
return NextResponse.json(
61+
{ error: 'API access chỉ dành cho gói Business trở lên' },
62+
{ status: 403 }
63+
)
64+
}
65+
66+
// Check limit (max 10 keys)
67+
const count = await prisma.apiKey.count({
68+
where: { userId: session.user.id }
69+
})
70+
71+
if (count >= 10) {
72+
return NextResponse.json(
73+
{ error: 'Đã đạt giới hạn 10 API keys' },
74+
{ status: 400 }
75+
)
76+
}
77+
78+
const body = await request.json()
79+
const { name } = body
80+
81+
if (!name || name.length < 3) {
82+
return NextResponse.json(
83+
{ error: 'Tên phải có ít nhất 3 ký tự' },
84+
{ status: 400 }
85+
)
86+
}
87+
88+
// Generate API key: as_xxxxxxxxxxxxxxxx
89+
const key = `as_${nanoid(32)}`
90+
const prefix = key.substring(0, 8)
91+
const hashedKey = await hash(key, 12)
92+
93+
const apiKey = await prisma.apiKey.create({
94+
data: {
95+
userId: session.user.id,
96+
name,
97+
key: hashedKey,
98+
prefix,
99+
},
100+
})
101+
102+
return NextResponse.json({
103+
success: true,
104+
key, // Return unhashed key ONLY this one time
105+
data: {
106+
id: apiKey.id,
107+
name: apiKey.name,
108+
prefix: apiKey.prefix,
109+
createdAt: apiKey.createdAt,
110+
},
111+
})
112+
113+
} catch (error) {
114+
console.error('API key create error:', error)
115+
return NextResponse.json(
116+
{ error: 'Failed to create API key' },
117+
{ status: 500 }
118+
)
119+
}
120+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import NextAuth from 'next-auth'
2+
import { authOptions } from '@/app/lib/auth'
3+
4+
const handler = NextAuth(authOptions)
5+
6+
export { handler as GET, handler as POST }

app/api/auth/register/route.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Authentication API Routes
3+
*/
4+
5+
import { NextRequest, NextResponse } from 'next/server'
6+
import { hash } from 'bcryptjs'
7+
import prisma from '@/app/lib/db'
8+
import { z } from 'zod'
9+
10+
const registerSchema = z.object({
11+
email: z.string().email('Email không hợp lệ'),
12+
password: z.string().min(8, 'Mật khẩu phải có ít nhất 8 ký tự'),
13+
name: z.string().optional(),
14+
})
15+
16+
export async function POST(request: NextRequest) {
17+
try {
18+
const body = await request.json()
19+
const { email, password, name } = registerSchema.parse(body)
20+
21+
// Check if user exists
22+
const existingUser = await prisma.user.findUnique({
23+
where: { email }
24+
})
25+
26+
if (existingUser) {
27+
return NextResponse.json(
28+
{ error: 'Email đã được sử dụng' },
29+
{ status: 400 }
30+
)
31+
}
32+
33+
// Hash password
34+
const hashedPassword = await hash(password, 12)
35+
36+
// Create user
37+
const user = await prisma.user.create({
38+
data: {
39+
email,
40+
password: hashedPassword,
41+
name,
42+
tier: 'FREE',
43+
role: 'USER',
44+
status: 'ACTIVE',
45+
}
46+
})
47+
48+
return NextResponse.json({
49+
success: true,
50+
user: {
51+
id: user.id,
52+
email: user.email,
53+
name: user.name,
54+
}
55+
})
56+
57+
} catch (error) {
58+
if (error instanceof z.ZodError) {
59+
return NextResponse.json(
60+
{ error: error.errors[0].message },
61+
{ status: 400 }
62+
)
63+
}
64+
65+
return NextResponse.json(
66+
{ error: 'Đã xảy ra lỗi khi đăng ký' },
67+
{ status: 500 }
68+
)
69+
}
70+
}

0 commit comments

Comments
 (0)