206 lines
5.6 KiB
TypeScript
206 lines
5.6 KiB
TypeScript
import { NextAuthOptions } from 'next-auth'
|
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
|
import { JWT } from 'next-auth/jwt'
|
|
|
|
declare module 'next-auth' {
|
|
interface Session {
|
|
user: {
|
|
name?: string | null
|
|
surname?: string | null
|
|
email?: string | null
|
|
phone_number?: string | null
|
|
image?: string | null
|
|
}
|
|
accessToken?: string
|
|
refreshToken?: string
|
|
expiresAt?: number
|
|
}
|
|
|
|
interface User {
|
|
id: string
|
|
email: string
|
|
name?: string
|
|
accessToken?: string
|
|
refreshToken?: string
|
|
}
|
|
|
|
interface JWT {
|
|
accessToken?: string
|
|
refreshToken?: string
|
|
expiresAt?: number
|
|
error?: string
|
|
}
|
|
}
|
|
|
|
interface GoogleToken extends JWT {
|
|
accessToken?: string
|
|
refreshToken?: string
|
|
expiresAt?: number | undefined
|
|
error?: string
|
|
}
|
|
|
|
async function refreshAccessToken(token: GoogleToken): Promise<GoogleToken> {
|
|
try {
|
|
const BACKEND = process.env.BACKEND_URL || 'http://127.0.0.1:8000/api/v1'
|
|
const response = await fetch(`${BACKEND}/auth/refresh/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
refresh: token.refreshToken,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
let errorData: { error?: string; [key: string]: unknown } = {}
|
|
try {
|
|
errorData = JSON.parse(errorText)
|
|
} catch {
|
|
errorData = { error: errorText }
|
|
}
|
|
|
|
const errorMessage = (errorData.error as string) || (errorData.detail as string) || ''
|
|
if (typeof errorMessage === 'string' &&
|
|
(errorMessage.includes('Token is expired') ||
|
|
errorMessage.includes('expired') ||
|
|
errorData.code === 'token_not_valid')) {
|
|
console.warn('Refresh token expired, user needs to re-authenticate')
|
|
return {
|
|
...token,
|
|
error: 'RefreshTokenExpired',
|
|
}
|
|
}
|
|
|
|
console.error('Token refresh failed:', errorData.error || 'Token refresh failed')
|
|
return {
|
|
...token,
|
|
error: 'RefreshAccessTokenError',
|
|
}
|
|
}
|
|
|
|
const refreshedTokens = await response.json()
|
|
|
|
return {
|
|
...token,
|
|
accessToken: refreshedTokens.access,
|
|
refreshToken: refreshedTokens.refresh ?? token.refreshToken,
|
|
expiresAt: refreshedTokens.expires_at,
|
|
}
|
|
} catch (error) {
|
|
console.error('Refresh error:', error)
|
|
return {
|
|
...token,
|
|
error: 'RefreshAccessTokenError',
|
|
}
|
|
}
|
|
}
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
providers: [
|
|
//логин
|
|
CredentialsProvider({
|
|
id: 'credentials',
|
|
name: 'Credentials',
|
|
credentials: {
|
|
login: { label: 'Login', type: 'text' },
|
|
password: { label: 'Password', type: 'password' },
|
|
role: { label: 'Role', type: 'text' },
|
|
},
|
|
async authorize(credentials) {
|
|
try {
|
|
const BACKEND = process.env.BACKEND_URL || 'http://127.0.0.1:8000/api/v1'
|
|
const res = await fetch(`${BACKEND}/auth/login/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
login: credentials?.login,
|
|
password: credentials?.password,
|
|
role: credentials?.role,
|
|
}),
|
|
})
|
|
|
|
const raw = await res.text()
|
|
let data: {
|
|
error?: string
|
|
user?: {
|
|
id: string | number
|
|
email: string
|
|
name: string
|
|
role: string
|
|
}
|
|
access?: string
|
|
refresh?: string
|
|
[key: string]: unknown
|
|
}
|
|
try {
|
|
data = JSON.parse(raw)
|
|
} catch {
|
|
data = { error: raw }
|
|
}
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || `Authentication failed (${res.status})`)
|
|
}
|
|
|
|
return {
|
|
id: data.user?.id?.toString?.() ?? String(data.user?.id),
|
|
email: data.user?.email ?? '',
|
|
name: data.user?.name ?? '', // backend uses `name`, not `firstName`
|
|
accessToken: data.access ?? '',
|
|
refreshToken: data.refresh ?? '',
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error)
|
|
return null
|
|
}
|
|
},
|
|
}),
|
|
],
|
|
callbacks: {
|
|
async jwt({ token, user, account }) {
|
|
if (user && account) {
|
|
if (account.type === 'credentials') {
|
|
return {
|
|
...token,
|
|
accessToken: user.accessToken,
|
|
refreshToken: user.refreshToken,
|
|
expiresAt: Math.floor(Date.now() / 1000) + 15 * 60, // 15 минут
|
|
}
|
|
} else {
|
|
return {
|
|
...token,
|
|
accessToken: account.access_token,
|
|
refreshToken: account.refresh_token,
|
|
expiresAt: account.expires_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
// проверяем не истекает ли токен в ближайшие 5 минут
|
|
const expiresAt = token.expiresAt as number | undefined
|
|
if (
|
|
typeof expiresAt === 'number' &&
|
|
Date.now() < (expiresAt - 5 * 60) * 1000 // обновляем за 5 минут до истечения
|
|
) {
|
|
return token
|
|
}
|
|
|
|
return refreshAccessToken(token as GoogleToken)
|
|
},
|
|
async session({ session, token }) {
|
|
if (token) {
|
|
session.accessToken = token.accessToken as string
|
|
session.refreshToken = token.refreshToken as string
|
|
|
|
if (token.error === 'RefreshTokenExpired') {
|
|
session.accessToken = undefined
|
|
session.refreshToken = undefined
|
|
}
|
|
}
|
|
return session
|
|
},
|
|
},
|
|
}
|