So, I deployed my frontend on Vercel and my API on Google Cloud Run with docker. It works fine on the development mode however, in production I can't log in with firebase auth.
Logging in with credentials works but its not redirecting the user to the dashboard and logging in with google doesn't creates session at all. On top of that, when I go to dashboard after logging in with credentials, It throws 401 authorization error. And yes, all of my environment variables are correct.
I think that is because something is wrong at my fastify.register(...) section in auth-routes.js page (especially I am suspicious about the sameSite property.) but I can't find what i am doing wrong.
NOTE: All of the problems above are only occurs when I use my app on production (deployed version from vercel). My app works fine on development mode.
auth-routes.js
const admin = require('firebase-admin');
const fp = require('fastify-plugin')
const cookie = require('@fastify/cookie')
const session = require('@fastify/session')
const oauth2 = require('@fastify/oauth2')
async function authPlugin(fastify, options) {
// Cookie plugin
fastify.register(cookie)
// Session plugin
fastify.register(session, {
cookieName: 'sessionId',
secret: process.env.SESSION_SECRET,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/',
},
saveUninitialized: false,
rolling: true
})
// Login için OAuth2
fastify.register(oauth2, {
name: 'googleOAuth2Login',
scope: ['email', 'profile'],
credentials: {
client: {
id: process.env.GOOGLE_CLIENT_ID,
secret: process.env.GOOGLE_CLIENT_SECRET
},
auth: oauth2.GOOGLE_CONFIGURATION
},
startRedirectPath: '/api/login/google',
callbackUri: `${process.env.API_URL}/api/login/google/callback`,
cookie: {
name: 'sessionId',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
}
})
// Register için OAuth2
fastify.register(oauth2, {
name: 'googleOAuth2Register',
scope: ['email', 'profile'],
credentials: {
client: {
id: process.env.GOOGLE_CLIENT_ID,
secret: process.env.GOOGLE_CLIENT_SECRET
},
auth: oauth2.GOOGLE_CONFIGURATION
},
startRedirectPath: '/api/register/google',
callbackUri: `${process.env.API_URL}/api/register/google/callback`,
cookie: {
name: 'sessionId',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
}
})
}
module.exports = fp(authPlugin, {
name: 'auth-plugin'
})
async function authRoutes(fastify, options) {
fastify.post('/api/login', async (request, reply) => {
try {
const { email, password } = request.body;
if (!email || !password) {
return reply.code(400).send({
success: false,
error: "Email and password are required"
});
}
// Firebase Auth REST API ile giriş yap
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${process.env.FIREBASE_API_KEY}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password,
returnSecureToken: true
})
});
const data = await response.json();
if (!response.ok) {
return reply.code(401).send({
success: false,
error: "Invalid credentials"
});
}
// Session'a kullanıcı bilgilerini kaydet
request.session.user = {
uid: data.localId,
email: data.email,
emailVerified: data.emailVerified
};
// Session'ı kaydet
await request.session.save();
// reply.send() kullanarak response gönder
return reply.send({
success: true,
token: data.idToken,
user: {
uid: data.localId,
email: data.email,
emailVerified: data.emailVerified
}
});
} catch (error) {
console.error('Login error:', error);
return reply.code(500).send({
success: false,
error: "Failed to login"
});
}
});
// Register route
fastify.post('/api/register', async (request, reply) => {
try {
const { email, password, name } = request.body;
if (!email || !password) {
return reply.code(400).send({
success: false,
error: "Email and password are required"
});
}
// Firebase Auth'da yeni kullanıcı oluştur
const userRecord = await admin.auth().createUser({
email,
password,
emailVerified: false,
displayName: name
});
// Firestore'da users collection'ında yeni document oluştur
const userDoc = {
uid: userRecord.uid,
email: userRecord.email,
name: name,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
plan: { planType: 'free', planLookupKey: 'free_plan' },
projects: [],
credits: 0
};
await admin.firestore().collection('users').doc(userRecord.uid).set(userDoc);
// Session oluştur
request.session.user = {
uid: userRecord.uid,
email: userRecord.email,
name: name,
emailVerified: false
};
// Session'ı kaydet
await request.session.save();
const customToken = await admin.auth().createCustomToken(userRecord.uid);
return reply.send({
success: true,
token: customToken,
user: {
uid: userRecord.uid,
email: userRecord.email,
}
});
} catch (error) {
console.error('Register error details:', {
code: error.code,
message: error.message,
stack: error.stack
});
// Email zaten kullanımda ise
if (error.code === 'auth/email-already-exists') {
return reply.code(400).send({
success: false,
error: "Email is already in use"
});
}
// Zayıf parola
if (error.code === 'auth/weak-password') {
return reply.code(400).send({
success: false,
error: "Password should be at least 6 characters"
});
}
// Diğer Firebase Auth hataları için
if (error.code?.startsWith('auth/')) {
return reply.code(400).send({
success: false,
error: error.message
});
}
return reply.code(500).send({
success: false,
error: "Failed to create user",
details: error.message
});
}
});
// Logout route'u ekle
fastify.post('/api/logout', async (request, reply) => {
try {
// Session'ı temizle
request.session.destroy();
// Cookie'yi temizle
reply.clearCookie('sessionId');
return reply.send({
success: true,
message: "Logged out successfully"
});
} catch (error) {
console.error('Logout error:', error);
return reply.code(500).send({
success: false,
error: "Failed to logout"
});
}
});
// Google login callback route'u
fastify.get('/api/login/google/callback', async function (request, reply) {
try {
const { token } = await this.googleOAuth2Login.getAccessTokenFromAuthorizationCodeFlow(request)
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${token.access_token}`
}
})
const googleUser = await userResponse.json()
let userRecord
let isNewUser = false
try {
userRecord = await admin.auth().getUserByEmail(googleUser.email)
} catch (error) {
if (error.code === 'auth/user-not-found') {
isNewUser = true
userRecord = await admin.auth().createUser({
email: googleUser.email,
emailVerified: true,
displayName: googleUser.name
})
await admin.firestore().collection('users').doc(userRecord.uid).set({
uid: userRecord.uid,
email: googleUser.email,
name: googleUser.name,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
plan: { planType: 'free', planLookupKey: 'free_plan' },
projects: [],
credits: 0
})
} else {
throw error
}
}
// Session oluştur
request.session.user = {
uid: userRecord.uid,
email: userRecord.email,
name: userRecord.displayName || googleUser.name,
emailVerified: true
}
await request.session.save()
// Custom token oluştur
const customToken = await admin.auth().createCustomToken(userRecord.uid)
// Token'ı cookie olarak gönder
reply.setCookie('auth_token', customToken, {
path: '/',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax'
})
// Kullanıcıyı dashboard'a yönlendir
return reply.redirect(`${process.env.APP_URL}/dashboard`);
} catch (error) {
console.error('Google auth error:', error)
return reply.redirect(`${process.env.APP_URL}/auth/error?error=${encodeURIComponent(error.message)}`)
}
})
// Google register callback route'u
fastify.get('/api/register/google/callback', async function (request, reply) {
try {
// Google'dan token al
const { token } = await this.googleOAuth2Register.getAccessTokenFromAuthorizationCodeFlow(request)
// Google kullanıcı bilgilerini al
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${token.access_token}`
}
})
const googleUser = await userResponse.json()
// Firebase'de kullanıcıyı kontrol et veya oluştur
let userRecord
let isNewUser = false
try {
// Mevcut kullanıcıyı bul
userRecord = await admin.auth().getUserByEmail(googleUser.email)
} catch (error) {
if (error.code === 'auth/user-not-found') {
isNewUser = true
// Yeni kullanıcı oluştur
userRecord = await admin.auth().createUser({
email: googleUser.email,
emailVerified: true,
displayName: googleUser.name
})
// Firestore'da kullanıcı dokümanı oluştur
await admin.firestore().collection('users').doc(userRecord.uid).set({
uid: userRecord.uid,
email: googleUser.email,
name: googleUser.name,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
plan: { planType: 'free', planLookupKey: 'free_plan' },
projects: [],
credits: 0
})
} else {
throw error
}
}
// Session oluştur
request.session.user = {
uid: userRecord.uid,
email: userRecord.email,
name: userRecord.displayName || googleUser.name,
emailVerified: true
}
await request.session.save()
// Custom token oluştur
const customToken = await admin.auth().createCustomToken(userRecord.uid)
const redirectUrl = `${process.env.APP_URL}/dashboard?token=${customToken}`;
return reply.redirect(redirectUrl);
} catch (error) {
console.error('Google auth error:', error)
return reply.redirect(`${process.env.APP_URL}/auth/error?error=${encodeURIComponent(error.message)}`)
}
})
// Şifre doğrulama route'u
fastify.post('/api/user/verify-password', async (request, reply) => {
try {
// Session kontrolü
if (!request.session?.user?.uid) {
return reply.code(400).send({
success: false,
error: "Unauthorized"
});
}
const { currentPassword } = request.body;
const userId = request.session.user.uid;
// Kullanıcıyı al
const userRecord = await admin.auth().getUser(userId);
const userEmail = userRecord.email;
// Firebase Auth REST API ile şifre doğrulama
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${process.env.FIREBASE_API_KEY}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: userEmail,
password: currentPassword,
returnSecureToken: true
})
});
if (!response.ok) {
return reply.code(401).send({
success: false,
error: "Invalid current password"
});
}
return reply.code(200).send({
success: true,
message: "Password verified successfully"
});
} catch (error) {
console.error('Password verification error:', error);
return reply.code(401).send({
success: false,
error: "Failed to verify password"
});
}
});
}
module.exports = {
authPlugin,
authRoutes
}
server.js
require('dotenv').config()
const fastify = require('fastify')({
logger: true,
bodyLimit: 2048 * 1024 * 1024 // 2GB limit
})
const cors = require('@fastify/cors')
const { Server } = require('socket.io')
const { authPlugin, authRoutes } = require('./routes/auth-routes')
const captionRoutes = require('./routes/caption-routes')
const imageRoutes = require('./routes/image-routes')
const storageRoutes = require('./routes/storage-routes')
const videoRoutes = require('./routes/video-routes')
const voiceRoutes = require('./routes/voice-routes')
const userRoutes = require('./routes/user-routes')
const paymentRoutes = require('./routes/payments')
// CORS settings
fastify.register(cors, {
origin: (origin, cb) => {
// Allow all origins in development environment
if (!origin || process.env.NODE_ENV === 'development') {
cb(null, true)
return
}
// Allowed origins
const allowedOrigins = [
process.env.APP_URL,
'http://localhost:3000',
'https://api.stripe.com',
]
if (allowedOrigins.indexOf(origin) !== -1) {
cb(null, true)
} else {
cb(new Error('Not allowed by CORS policy'), false)
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Stripe-Signature', 'Cookie']
})
// Remove @fastify/websocket plugin
// fastify.register(require('@fastify/websocket'))
// Create Socket.IO object first and add as decorator
let io = null;
fastify.decorate('io', {
// Initialize as empty object
emit: (event, data) => {
console.log(`Socket.IO not initialized yet. Event: ${event}`, data);
}
});
// Register all routes
fastify.register(authPlugin)
fastify.register(authRoutes)
fastify.register(captionRoutes)
fastify.register(imageRoutes)
fastify.register(storageRoutes)
fastify.register(videoRoutes)
fastify.register(voiceRoutes)
fastify.register(userRoutes)
fastify.register(paymentRoutes)
// Start server
const start = async () => {
try {
await fastify.listen({ port: 8080, host: '0.0.0.0' })
// Connect Socket.IO server to Fastify's HTTP server
// DISABLE CORS settings for Socket.IO - Use Fastify's CORS settings
io = new Server(fastify.server, {
cors: false // Disable CORS
});
// Listen for Socket.IO connections
io.on('connection', (socket) => {
fastify.log.info(`Client connected: ${socket.id}`);
socket.on('disconnect', () => {
fastify.log.info(`Client disconnected: ${socket.id}`);
});
});
// Update decorator (without redefining)
fastify.io = io;
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
// Export Fastify
module.exports = fastify