Files
tripwithbonus/frontend/app/api/auth/[...nextauth]/route.ts

353 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import NextAuth, { NextAuthOptions } from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
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
}
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: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
name: { label: 'Name', type: 'text' },
phone_number: { label: 'Phone Number', type: 'tel' },
privacy_accepted: { label: 'Privacy Accepted', type: 'boolean' },
},
async authorize(credentials) {
try {
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,
phone_number: credentials?.phone_number,
privacy_accepted: credentials?.privacy_accepted === 'true',
}),
}
)
const data = await res.json()
if (!res.ok) {
throw new Error(
data.error || data.details?.toString() || 'Registration 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('Registration error:', error)
return null
}
},
}),
//регистрация менеджера
CredentialsProvider({
id: 'register-credentials-manager',
name: 'RegisterManager',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
username: { label: 'Username', type: 'text' },
},
async authorize(credentials) {
try {
const res = await fetch(
`${process.env.BACKEND_URL}/register/business/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
username: credentials?.username,
}),
}
)
if (!res.ok) {
const text = await res.text()
console.error('Backend error response:', text)
throw new Error('Registration failed: ' + text)
}
const data = await res.json()
return {
id: data.user.id.toString(),
email: data.user.email,
name: data.user.firstName,
accessToken: data.access,
refreshToken: data.refresh,
userType: data.user.userType || 'manager',
}
} catch (error) {
console.error('Registration error:', error)
return null
}
},
}),
//логин обычный
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
}
},
}),
//логин для менеджеров
CredentialsProvider({
id: 'login-credentials-manager',
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/business/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
}),
}
)
const data = await res.json()
// console.log('Business login response:', data)
if (!res.ok) {
throw new Error(data.error || 'Authentication failed')
}
return {
id: data.user.id.toString(),
email: data.user.email || `${data.user.phone_number}@example.com`,
name: data.user.username || data.user.title,
accessToken: data.access,
refreshToken: data.refresh,
userType: data.user.userType || 'manager',
}
} 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 }