272 lines
7.9 KiB
TypeScript
272 lines
7.9 KiB
TypeScript
import NextAuth, { 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
|
||
userType?: string | null
|
||
}
|
||
accessToken?: string
|
||
refreshToken?: string
|
||
expiresAt?: number
|
||
}
|
||
|
||
interface User {
|
||
id: string
|
||
email: string
|
||
name?: string
|
||
accessToken?: string
|
||
refreshToken?: string
|
||
userType?: string
|
||
}
|
||
|
||
interface JWT {
|
||
accessToken?: string
|
||
refreshToken?: string
|
||
expiresAt?: number
|
||
error?: string
|
||
}
|
||
}
|
||
|
||
interface GoogleToken extends JWT {
|
||
accessToken?: string
|
||
refreshToken?: string
|
||
expiresAt?: number | undefined
|
||
error?: string
|
||
}
|
||
|
||
export const authOptions: NextAuthOptions = {
|
||
providers: [
|
||
//google login flow
|
||
// GoogleProvider({
|
||
// clientId: process.env.GOOGLE_CLIENT_ID!,
|
||
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||
// }),
|
||
|
||
//регистрация клиента
|
||
CredentialsProvider({
|
||
id: 'register-credentials',
|
||
name: 'Register',
|
||
credentials: {
|
||
name: { label: 'Name', type: 'text' },
|
||
surname: { label: 'Surname', type: 'text' },
|
||
email: { label: 'Email', type: 'email' },
|
||
password: { label: 'Password', type: 'password' },
|
||
phone_number: { label: 'Phone Number', type: 'tel' },
|
||
privacy_accepted: { label: 'Privacy Accepted', type: 'boolean' },
|
||
},
|
||
async authorize(credentials) {
|
||
try {
|
||
if (
|
||
!credentials?.email ||
|
||
!credentials?.password ||
|
||
!credentials?.name ||
|
||
!credentials?.phone_number ||
|
||
!credentials?.privacy_accepted ||
|
||
!credentials?.surname
|
||
) {
|
||
throw new Error('Все поля обязательны для заполнения')
|
||
}
|
||
|
||
const res = await fetch(`${process.env.BACKEND_URL}/register/clients/`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email: credentials.email,
|
||
password: credentials.password,
|
||
name: credentials.name,
|
||
surname: credentials.surname,
|
||
phone_number: credentials.phone_number,
|
||
privacy_accepted: credentials.privacy_accepted === 'true',
|
||
}),
|
||
})
|
||
|
||
const data = await res.json()
|
||
|
||
if (!res.ok) {
|
||
console.error('Registration error response:', data)
|
||
const errorMessage =
|
||
typeof data === 'object'
|
||
? data.error || Object.values(data).flat().join(', ')
|
||
: 'Registration failed'
|
||
throw new Error(errorMessage)
|
||
}
|
||
|
||
return {
|
||
id: data.user.id.toString(),
|
||
email: data.user.email,
|
||
name: data.user.name,
|
||
surname: data.user.surname,
|
||
accessToken: data.access,
|
||
refreshToken: data.refresh,
|
||
}
|
||
} catch (error) {
|
||
console.error('Registration error:', error)
|
||
throw error
|
||
}
|
||
},
|
||
}),
|
||
|
||
//логин
|
||
CredentialsProvider({
|
||
name: 'Credentials',
|
||
credentials: {
|
||
email: { label: 'Email', type: 'email' },
|
||
password: { label: 'Password', type: 'password' },
|
||
},
|
||
async authorize(credentials) {
|
||
try {
|
||
const res = await fetch(`${process.env.BACKEND_URL}/auth/login/clients/`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email: credentials?.email,
|
||
password: credentials?.password,
|
||
}),
|
||
})
|
||
|
||
const data = await res.json()
|
||
|
||
if (!res.ok) {
|
||
throw new Error(data.error || 'Authentication failed')
|
||
}
|
||
|
||
return {
|
||
id: data.user.id.toString(),
|
||
email: data.user.email,
|
||
name: data.user.firstName,
|
||
accessToken: data.access,
|
||
refreshToken: data.refresh,
|
||
}
|
||
} catch (error) {
|
||
console.error('Login error:', error)
|
||
return null
|
||
}
|
||
},
|
||
}),
|
||
],
|
||
callbacks: {
|
||
async jwt({ token, user, account }) {
|
||
// console.log('JWT Callback - User:', user?.userType)
|
||
// console.log('JWT Callback - Account:', account?.type)
|
||
|
||
if (user && account) {
|
||
if (account.type === 'credentials') {
|
||
// console.log('Adding userType to token:', user.userType)
|
||
return {
|
||
...token,
|
||
accessToken: user.accessToken,
|
||
refreshToken: user.refreshToken,
|
||
expiresAt: Math.floor(Date.now() / 1000) + 15 * 60, // 15 минут
|
||
userType: user.userType,
|
||
}
|
||
} else {
|
||
return {
|
||
...token,
|
||
accessToken: account.access_token,
|
||
refreshToken: account.refresh_token,
|
||
expiresAt: account.expires_at,
|
||
userType: user.userType,
|
||
}
|
||
}
|
||
}
|
||
|
||
// проверяем не истекает ли токен в ближайшие 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 }) {
|
||
// console.log('Session Callback - Token:', token)
|
||
// console.log('Session Callback - userType:', token.userType)
|
||
|
||
if (token) {
|
||
session.user.userType = token.userType as string
|
||
session.accessToken = token.accessToken as string
|
||
session.refreshToken = token.refreshToken as string
|
||
|
||
// console.log('Session Callback - Final session:', session)
|
||
}
|
||
return session
|
||
},
|
||
async signIn({ account }) {
|
||
if (account?.access_token && typeof account.access_token === 'string') {
|
||
try {
|
||
const res = await fetch(`${process.env.BACKEND_URL}/auth/google/`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token: account.id_token }),
|
||
})
|
||
|
||
if (!res.ok) {
|
||
console.error('Failed to authenticate with Django')
|
||
return false
|
||
}
|
||
|
||
const djangoTokens = await res.json()
|
||
// сторим токены бека, а не гугла
|
||
account.access_token = djangoTokens.access
|
||
account.refresh_token = djangoTokens.refresh
|
||
account.expires_at = Math.floor(Date.now() / 1000) + 5 * 60 // 5 минут
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error sending token to backend:', error)
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
},
|
||
},
|
||
}
|
||
|
||
async function refreshAccessToken(token: GoogleToken): Promise<GoogleToken> {
|
||
try {
|
||
const response = await fetch(`${process.env.BACKEND_URL}/auth/refresh/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
refresh: token.refreshToken,
|
||
}),
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json()
|
||
|
||
throw errorData
|
||
}
|
||
|
||
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',
|
||
}
|
||
}
|
||
}
|
||
|
||
const handler = NextAuth(authOptions)
|
||
export { handler as GET, handler as POST }
|