Initial reader-api backend extracted from reader

This commit is contained in:
2026-03-24 13:55:10 +07:00
parent 56f8f5ccfc
commit 24f070d14e
69 changed files with 12167 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
import NextAuth from "next-auth"
import { authOptions } from "@/lib/auth"
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
+95
View File
@@ -0,0 +1,95 @@
import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma"
import { OAuth2Client } from "google-auth-library"
import { SignJWT, importPKCS8, generateKeyPair } from "jose"
import * as crypto from "crypto"
const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID)
function generateTokens(userId: string) {
const secret = process.env.MOBILE_JWT_SECRET || process.env.NEXTAUTH_SECRET || ""
const key = crypto.createHmac("sha256", secret)
const payload = Buffer.from(JSON.stringify({ sub: userId, iat: Math.floor(Date.now() / 1000) })).toString("base64url")
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url")
const sig = key.update(`${header}.${payload}`).digest("base64url")
const accessToken = `${header}.${payload}.${sig}`
// refresh token: random 40-byte hex, stored hashed in DB if needed; for now return raw
const refreshToken = crypto.randomBytes(40).toString("hex")
return { accessToken, refreshToken }
}
export async function POST(req: Request) {
try {
const body = await req.json()
const { googleIdToken } = body
if (!googleIdToken || typeof googleIdToken !== "string") {
return NextResponse.json({ error: "googleIdToken is required" }, { status: 400 })
}
// Verify the Google ID token
let ticket
try {
ticket = await googleClient.verifyIdToken({
idToken: googleIdToken,
audience: process.env.GOOGLE_CLIENT_ID,
})
} catch {
return NextResponse.json({ error: "Invalid Google token" }, { status: 401 })
}
const payload = ticket.getPayload()
if (!payload?.email) {
return NextResponse.json({ error: "Unable to extract email from token" }, { status: 401 })
}
const { email, name, picture, sub: googleSub } = payload
// Upsert user — match NextAuth behaviour
let user = await prisma.user.findUnique({ where: { email } })
if (!user) {
user = await prisma.user.create({
data: {
email,
name: name ?? null,
image: picture ?? null,
emailVerified: new Date(),
},
})
}
// Upsert Google Account link
const existingAccount = await prisma.account.findUnique({
where: { provider_providerAccountId: { provider: "google", providerAccountId: googleSub } },
})
if (!existingAccount) {
await prisma.account.create({
data: {
userId: user.id,
type: "oauth",
provider: "google",
providerAccountId: googleSub,
},
})
}
const { accessToken, refreshToken } = generateTokens(user.id)
return NextResponse.json({
accessToken,
refreshToken,
expiresIn: 3600,
user: {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
role: user.role,
},
})
} catch (error) {
console.error("Mobile login error:", error)
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
}
}