diff --git a/backend/main.go b/backend/main.go
index e76e0c1f..429c8378 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -82,13 +82,7 @@ func main() {
store := sessions.NewCookieStore(sessionKey)
// Configure secure cookie options
- store.Options = &sessions.Options{
- Path: "/",
- MaxAge: 86400 * 7, // 7 days
- HttpOnly: true, // Prevent JavaScript access
- Secure: os.Getenv("ENV") == "production", // HTTPS only in production
- SameSite: http.SameSiteLaxMode, // CSRF protection (Lax allows OAuth redirects)
- }
+ store.Options = sessionCookieOptions()
gob.Register(map[string]interface{}{})
diff --git a/backend/session_options.go b/backend/session_options.go
new file mode 100644
index 00000000..a7327983
--- /dev/null
+++ b/backend/session_options.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "net/http"
+ "os"
+
+ "github.com/gorilla/sessions"
+)
+
+func sessionCookieOptions() *sessions.Options {
+ return &sessions.Options{
+ Path: "/",
+ MaxAge: 86400 * 7, // 7 days
+ HttpOnly: true, // Prevent JavaScript access
+ Secure: os.Getenv("ENV") == "production", // HTTPS only in production
+ SameSite: http.SameSiteLaxMode, // CSRF protection (Lax allows OAuth redirects)
+ }
+}
diff --git a/backend/session_options_test.go b/backend/session_options_test.go
new file mode 100644
index 00000000..313137f5
--- /dev/null
+++ b/backend/session_options_test.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSessionCookieOptions_Production(t *testing.T) {
+ t.Setenv("ENV", "production")
+
+ opts := sessionCookieOptions()
+
+ assert.Equal(t, "/", opts.Path)
+ assert.Equal(t, 86400*7, opts.MaxAge)
+ assert.True(t, opts.HttpOnly)
+ assert.True(t, opts.Secure)
+ assert.Equal(t, http.SameSiteLaxMode, opts.SameSite)
+}
+
+func TestSessionCookieOptions_NonProduction(t *testing.T) {
+ t.Setenv("ENV", "development")
+
+ opts := sessionCookieOptions()
+
+ assert.Equal(t, 86400*7, opts.MaxAge)
+ assert.True(t, opts.HttpOnly)
+ assert.False(t, opts.Secure)
+}
+
+func TestSessionCookieOptions_UnsetENV(t *testing.T) {
+ t.Setenv("ENV", "")
+
+ opts := sessionCookieOptions()
+
+ assert.Equal(t, 86400*7, opts.MaxAge)
+ assert.False(t, opts.Secure)
+}
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index e176c8bc..68f83ceb 100644
--- a/frontend/src/components/LandingPage.tsx
+++ b/frontend/src/components/LandingPage.tsx
@@ -1,3 +1,6 @@
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router';
+import { url } from '@/components/utils/URLs';
import { About } from './LandingComponents/About/About';
import { FAQ } from './LandingComponents/FAQ/FAQ';
import { Footer } from './LandingComponents/Footer/Footer';
@@ -9,6 +12,26 @@ import { Contact } from './LandingComponents/Contact/Contact';
import '../App.css';
export const LandingPage = () => {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const redirectIfLoggedIn = async () => {
+ try {
+ const response = await fetch(url.backendURL + 'api/user', {
+ method: 'GET',
+ credentials: 'include',
+ });
+ if (response.ok) {
+ navigate('/home');
+ }
+ } catch (error) {
+ console.error('Error checking login status:', error);
+ }
+ };
+
+ redirectIfLoggedIn();
+ }, [navigate]);
+
return (
diff --git a/frontend/src/components/__tests__/LandingPage.test.tsx b/frontend/src/components/__tests__/LandingPage.test.tsx
index 79880182..564715da 100644
--- a/frontend/src/components/__tests__/LandingPage.test.tsx
+++ b/frontend/src/components/__tests__/LandingPage.test.tsx
@@ -1,6 +1,11 @@
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import { LandingPage } from '../LandingPage';
+const mockedNavigate = jest.fn();
+const consoleErrorSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
// Mock dependencies
jest.mock('../LandingComponents/Navbar/Navbar', () => ({
Navbar: () =>
Mocked Navbar
,
@@ -27,7 +32,28 @@ jest.mock('../../components/utils/ScrollToTop', () => ({
ScrollToTop: () =>
Mocked ScrollToTop
,
}));
+jest.mock('react-router', () => ({
+ useNavigate: () => mockedNavigate,
+}));
+
+jest.mock('@/components/utils/URLs', () => ({
+ url: {
+ backendURL: 'http://mocked-backend-url/',
+ },
+}));
+
+global.fetch = jest.fn(() =>
+ Promise.resolve({
+ ok: false,
+ })
+) as jest.Mock;
+
describe('LandingPage', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ consoleErrorSpy.mockClear();
+ });
+
it('renders all components correctly', () => {
render(
);
@@ -40,6 +66,45 @@ describe('LandingPage', () => {
expect(screen.getByText('Mocked Footer')).toBeInTheDocument();
expect(screen.getByText('Mocked ScrollToTop')).toBeInTheDocument();
});
+
+ it('redirects to /home when the user is already logged in', async () => {
+ (fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(mockedNavigate).toHaveBeenCalledWith('/home');
+ });
+ expect(fetch).toHaveBeenCalledWith('http://mocked-backend-url/api/user', {
+ method: 'GET',
+ credentials: 'include',
+ });
+ });
+
+ it('stays on the landing page when the user is not logged in', async () => {
+ render(
);
+
+ await waitFor(() => {
+ expect(fetch).toHaveBeenCalledWith('http://mocked-backend-url/api/user', {
+ method: 'GET',
+ credentials: 'include',
+ });
+ });
+ expect(mockedNavigate).not.toHaveBeenCalled();
+ });
+
+ it('stays on the landing page when the session check fails', async () => {
+ (fetch as jest.Mock).mockRejectedValueOnce(new Error('network error'));
+
+ render(
);
+
+ await waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalled();
+ });
+ expect(mockedNavigate).not.toHaveBeenCalled();
+ });
});
describe('LandingPage Component using Snapshot', () => {
diff --git a/production/backend-deployment.yaml b/production/backend-deployment.yaml
index 24b1ed38..4fe1a67f 100644
--- a/production/backend-deployment.yaml
+++ b/production/backend-deployment.yaml
@@ -40,6 +40,11 @@ spec:
configMapKeyRef:
key: CONTAINER_ORIGIN
name: backend-env
+ - name: ENV
+ valueFrom:
+ configMapKeyRef:
+ key: ENV
+ name: backend-env
- name: FRONTEND_ORIGIN_DEV
valueFrom:
configMapKeyRef:
diff --git a/production/backend-env-configmap.yaml b/production/backend-env-configmap.yaml
index 7bcf15ed..0e1fc8b0 100644
--- a/production/backend-env-configmap.yaml
+++ b/production/backend-env-configmap.yaml
@@ -3,6 +3,7 @@ data:
CLIENT_ID: "YOUR_GOOGLE_CLOUD_AUTH_CLIENT_ID" # Replace this in order to access the frontend
CLIENT_SEC: "YOUR_GOOGLE_CLOUD_AUTH_CLIENT_SECRET" # Replace this in order to access the frontend
CONTAINER_ORIGIN: http://syncserver:8080/
+ ENV: "production" # Required for secure HTTPS-only cookies
FRONTEND_ORIGIN_DEV: http://localhost
PORT: "8000"
REDIRECT_URL_DEV: http://localhost:8000/auth/callback