first commit
This commit is contained in:
146
lib/auth.ts
Normal file
146
lib/auth.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { NextAuthOptions } from 'next-auth'
|
||||
import type { JWT } from 'next-auth/jwt'
|
||||
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||
import GitHubProvider from 'next-auth/providers/github'
|
||||
import GoogleProvider from 'next-auth/providers/google'
|
||||
import {
|
||||
fetchRefreshedBackendJwt,
|
||||
getJwtExpMs,
|
||||
shouldRefreshBackendToken,
|
||||
} from '@/lib/backend-jwt-refresh'
|
||||
|
||||
const API_BASE = process.env.API_BASE_URL ?? 'http://localhost:8080'
|
||||
|
||||
type AuthUser = {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
accessTokenExpires: number
|
||||
}
|
||||
|
||||
async function refreshAccessToken(token: JWT): Promise<JWT> {
|
||||
const next = await fetchRefreshedBackendJwt(token)
|
||||
if (!next) {
|
||||
return {
|
||||
...token,
|
||||
error: 'RefreshAccessTokenError',
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
/** NextAuth oturum çerezi ömrü (saniye) — backend refresh ile uyumlu uzun süre */
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET,
|
||||
providers: [
|
||||
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
||||
? [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
|
||||
? [
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
CredentialsProvider({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const email = credentials?.email
|
||||
const password = credentials?.password
|
||||
if (!email || !password) return null
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
|
||||
if (!res.ok) return null
|
||||
|
||||
const data = (await res.json()) as { access: string; refresh: string }
|
||||
const accessToken = data.access
|
||||
const refreshToken = data.refresh
|
||||
|
||||
return {
|
||||
id: email,
|
||||
email,
|
||||
username: email,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpires: getJwtExpMs(accessToken),
|
||||
} satisfies AuthUser
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
const authUser = user as AuthUser
|
||||
|
||||
// OAuth login akışında backend access/refresh token'ı yoksa refresh denemeyelim.
|
||||
if (!authUser.accessToken || !authUser.refreshToken) {
|
||||
return {
|
||||
...token,
|
||||
user: {
|
||||
id: authUser.id,
|
||||
email: authUser.email,
|
||||
username: authUser.username ?? authUser.email,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: authUser.accessToken,
|
||||
refreshToken: authUser.refreshToken,
|
||||
accessTokenExpires: authUser.accessTokenExpires,
|
||||
user: {
|
||||
id: authUser.id,
|
||||
email: authUser.email,
|
||||
username: authUser.username,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (token.error === 'RefreshAccessTokenError') {
|
||||
return token
|
||||
}
|
||||
|
||||
if (!shouldRefreshBackendToken(token)) {
|
||||
return token
|
||||
}
|
||||
|
||||
return refreshAccessToken(token)
|
||||
},
|
||||
async session({ session, token }) {
|
||||
session.user = {
|
||||
...(session.user ?? {}),
|
||||
...(token.user ?? {}),
|
||||
}
|
||||
session.accessToken = token.accessToken
|
||||
session.refreshToken = token.refreshToken
|
||||
session.error = token.error
|
||||
return session
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: '/auth/login',
|
||||
},
|
||||
}
|
||||
116
lib/backend-jwt-refresh.ts
Normal file
116
lib/backend-jwt-refresh.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { JWT } from 'next-auth/jwt'
|
||||
import { encode } from 'next-auth/jwt'
|
||||
|
||||
const API_BASE = process.env.API_BASE_URL ?? 'http://localhost:8080'
|
||||
|
||||
/** Access token bitiminden önce yenile (ms) — backend 15 dk ise güvenli tampon */
|
||||
const REFRESH_BUFFER_MS = 120_000
|
||||
|
||||
/** NextAuth JWT şifreli cookie ömrü (saniye) — varsayılan NextAuth ile uyumlu */
|
||||
const JWT_COOKIE_MAX_AGE_SEC = 30 * 24 * 60 * 60
|
||||
|
||||
export function sessionCookieName(): string {
|
||||
return isSecureSessionCookieEnabled()
|
||||
? '__Secure-next-auth.session-token'
|
||||
: 'next-auth.session-token'
|
||||
}
|
||||
|
||||
export function getJwtExpMs(accessToken: string): number {
|
||||
try {
|
||||
const payloadPart = accessToken.split('.')[1]
|
||||
if (!payloadPart) return Date.now()
|
||||
const payload = JSON.parse(Buffer.from(payloadPart, 'base64url').toString('utf8')) as {
|
||||
exp?: number
|
||||
}
|
||||
if (!payload.exp) return Date.now()
|
||||
return payload.exp * 1000
|
||||
} catch {
|
||||
return Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/** Access süresi dolmak üzereyse veya dolmuşsa true */
|
||||
export function shouldRefreshBackendToken(token: JWT | null): boolean {
|
||||
if (!token?.refreshToken) return false
|
||||
const exp = token.accessTokenExpires as number | undefined
|
||||
if (!exp) return true
|
||||
return Date.now() >= exp - REFRESH_BUFFER_MS
|
||||
}
|
||||
|
||||
export async function fetchRefreshedBackendJwt(token: JWT): Promise<JWT | null> {
|
||||
const refreshToken = token.refreshToken
|
||||
if (!refreshToken) return null
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
})
|
||||
|
||||
if (!res.ok) return null
|
||||
|
||||
const data = (await res.json()) as { access?: unknown; refresh?: unknown }
|
||||
// Backend: { "access": "...", "refresh": "..." } — Login_Register.md ile aynı
|
||||
if (typeof data.access !== 'string' || !data.access) return null
|
||||
|
||||
const accessToken = data.access
|
||||
const nextRefresh =
|
||||
typeof data.refresh === 'string' && data.refresh.length > 0
|
||||
? data.refresh
|
||||
: refreshToken
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken,
|
||||
refreshToken: nextRefresh,
|
||||
accessTokenExpires: getJwtExpMs(accessToken),
|
||||
error: undefined,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function encodeSessionJwt(token: JWT): Promise<string> {
|
||||
const secret = process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET
|
||||
if (!secret) throw new Error('NEXTAUTH_SECRET eksik')
|
||||
|
||||
return encode({
|
||||
token,
|
||||
secret,
|
||||
maxAge: JWT_COOKIE_MAX_AGE_SEC,
|
||||
})
|
||||
}
|
||||
|
||||
export function sessionCookieOptions(): {
|
||||
httpOnly: boolean
|
||||
secure: boolean
|
||||
sameSite: 'lax'
|
||||
path: string
|
||||
maxAge: number
|
||||
} {
|
||||
const secure = isSecureSessionCookieEnabled()
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: JWT_COOKIE_MAX_AGE_SEC,
|
||||
}
|
||||
}
|
||||
|
||||
export function isSecureSessionCookieEnabled(): boolean {
|
||||
return (
|
||||
!!process.env.NEXTAUTH_URL?.startsWith('https://') ||
|
||||
!!process.env.VERCEL
|
||||
)
|
||||
}
|
||||
|
||||
/** Server action / Route Handler: güncellenmiş JWT’yi NextAuth session çerezine yazar */
|
||||
export function applySessionCookie(
|
||||
cookieStore: { set: (name: string, value: string, options: Record<string, unknown>) => void },
|
||||
jwt: string
|
||||
): void {
|
||||
cookieStore.set(sessionCookieName(), jwt, sessionCookieOptions())
|
||||
}
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user