From 4524f96692f59dc2383449ba97158143e1e927e4 Mon Sep 17 00:00:00 2001
From: Vasist10 <155972527+Vasist10@users.noreply.github.com>
Date: Mon, 9 Feb 2026 00:45:10 +0530
Subject: [PATCH 1/3] fix: secure session handling and landing page redirect
---
backend/controllers/app_handlers.go | 6 ++---
backend/main.go | 8 +++---
backend/middleware/auth.go | 5 ++++
backend/utils/session.go | 22 ++++++++++++++++
frontend/src/components/LandingPage.tsx | 24 ++++++++++++++++++
.../components/__tests__/LandingPage.test.tsx | 25 +++++++++++++++++--
6 files changed, 81 insertions(+), 9 deletions(-)
create mode 100644 backend/utils/session.go
diff --git a/backend/controllers/app_handlers.go b/backend/controllers/app_handlers.go
index 61ac915d..7216e3c6 100644
--- a/backend/controllers/app_handlers.go
+++ b/backend/controllers/app_handlers.go
@@ -52,7 +52,7 @@ func (a *App) OAuthHandler(w http.ResponseWriter, r *http.Request) {
// Store state in session for validation in callback
session, _ := a.SessionStore.Get(r, "session-name")
session.Values["oauth_state"] = state
- if err := session.Save(r, w); err != nil {
+ if err := utils.SaveSessionWithSecureCookie(session, r, w); err != nil {
utils.Logger.Errorf("Failed to save OAuth state to session: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -125,7 +125,7 @@ func (a *App) OAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
userInfo["uuid"] = uuidStr
userInfo["encryption_secret"] = encryptionSecret
session.Values["user"] = userInfo
- if err := session.Save(r, w); err != nil {
+ if err := utils.SaveSessionWithSecureCookie(session, r, w); err != nil {
utils.Logger.Errorf("Failed to save session: %v", err)
http.Error(w, "Session error", http.StatusInternalServerError)
return
@@ -221,7 +221,7 @@ func (a *App) EnableCORS(handler http.Handler) http.Handler {
func (a *App) LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := a.SessionStore.Get(r, "session-name")
session.Options.MaxAge = -1
- if err := session.Save(r, w); err != nil {
+ if err := utils.SaveSessionWithSecureCookie(session, r, w); err != nil {
utils.Logger.Errorf("Failed to clear session on logout: %v", err)
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
diff --git a/backend/main.go b/backend/main.go
index e76e0c1f..d27e4b78 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -84,10 +84,10 @@ func main() {
// 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)
+ MaxAge: 86400 * 30, // 30 days
+ HttpOnly: true, // Prevent JavaScript access
+ Secure: false, // Handled dynamically by SaveSessionWithSecureCookie / IsSecure
+ SameSite: http.SameSiteLaxMode, // CSRF protection (Lax allows OAuth redirects)
}
gob.Register(map[string]interface{}{})
diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go
index 8349fe69..681f0f6d 100644
--- a/backend/middleware/auth.go
+++ b/backend/middleware/auth.go
@@ -51,6 +51,11 @@ func AuthMiddleware(store *sessions.CookieStore) func(http.Handler) http.Handler
return
}
+ // Inject session credentials into headers for GET requests
+ r.Header.Set("X-User-Email", sessionEmail)
+ r.Header.Set("X-User-UUID", sessionUUID)
+ r.Header.Set("X-Encryption-Secret", sessionSecret)
+
// For POST requests with JSON body, inject session credentials
if r.Method == http.MethodPost && r.Body != nil {
// Read the body
diff --git a/backend/utils/session.go b/backend/utils/session.go
new file mode 100644
index 00000000..ae2958f2
--- /dev/null
+++ b/backend/utils/session.go
@@ -0,0 +1,22 @@
+package utils
+
+import (
+ "net/http"
+
+ "github.com/gorilla/sessions"
+)
+
+func IsSecure(r *http.Request) bool {
+ if r.TLS != nil {
+ return true
+ }
+ return r.Header.Get("X-Forwarded-Proto") == "https"
+}
+
+func SaveSessionWithSecureCookie(session *sessions.Session, r *http.Request, w http.ResponseWriter) error {
+ original := session.Options.Secure
+ session.Options.Secure = IsSecure(r)
+ err := session.Save(r, w)
+ session.Options.Secure = original
+ return err
+}
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index e176c8bc..4ca0e2d0 100644
--- a/frontend/src/components/LandingPage.tsx
+++ b/frontend/src/components/LandingPage.tsx
@@ -8,7 +8,31 @@ import { ScrollToTop } from '../components/utils/ScrollToTop';
import { Contact } from './LandingComponents/Contact/Contact';
import '../App.css';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { url } from './utils/URLs';
+
export const LandingPage = () => {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const checkLoginStatus = 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);
+ }
+ };
+
+ checkLoginStatus();
+ }, [navigate]);
+
return (
diff --git a/frontend/src/components/__tests__/LandingPage.test.tsx b/frontend/src/components/__tests__/LandingPage.test.tsx
index 79880182..b3234751 100644
--- a/frontend/src/components/__tests__/LandingPage.test.tsx
+++ b/frontend/src/components/__tests__/LandingPage.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
+import { BrowserRouter } from 'react-router-dom';
import { LandingPage } from '../LandingPage';
// Mock dependencies
@@ -27,9 +28,25 @@ jest.mock('../../components/utils/ScrollToTop', () => ({
ScrollToTop: () =>
Mocked ScrollToTop
,
}));
+// Mock fetch for auth check
+global.fetch = jest.fn(() =>
+ Promise.resolve({
+ ok: false,
+ status: 401,
+ } as Response)
+);
+
describe('LandingPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('renders all components correctly', () => {
- render(
);
+ render(
+
+
+
+ );
expect(screen.getByText('Mocked Navbar')).toBeInTheDocument();
expect(screen.getByText('Mocked Hero')).toBeInTheDocument();
@@ -44,7 +61,11 @@ describe('LandingPage', () => {
describe('LandingPage Component using Snapshot', () => {
it('renders landing page correctly', () => {
- const { asFragment } = render(
);
+ const { asFragment } = render(
+
+
+
+ );
expect(asFragment()).toMatchSnapshot('landing-page');
});
});
From b2b3df623506110a72f4953d67adce80173188ba Mon Sep 17 00:00:00 2001
From: Vasist
Date: Tue, 26 May 2026 23:58:19 +0530
Subject: [PATCH 2/3] test: add tests for session.go
---
backend/utils/session_test.go | 110 ++++++++++++++++++++++++++++++++++
1 file changed, 110 insertions(+)
create mode 100644 backend/utils/session_test.go
diff --git a/backend/utils/session_test.go b/backend/utils/session_test.go
new file mode 100644
index 00000000..ee53c289
--- /dev/null
+++ b/backend/utils/session_test.go
@@ -0,0 +1,110 @@
+package utils
+
+import (
+ "crypto/tls"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gorilla/sessions"
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_IsSecure(t *testing.T) {
+ tests := []struct {
+ name string
+ setupReq func(r *http.Request)
+ wantSecure bool
+ }{
+ {
+ name: "plain HTTP — no TLS, no header",
+ setupReq: func(r *http.Request) {},
+ wantSecure: false,
+ },
+ {
+ name: "TLS connection",
+ setupReq: func(r *http.Request) { r.TLS = &tls.ConnectionState{} },
+ wantSecure: true,
+ },
+ {
+ name: "X-Forwarded-Proto: https",
+ setupReq: func(r *http.Request) { r.Header.Set("X-Forwarded-Proto", "https") },
+ wantSecure: true,
+ },
+ {
+ name: "X-Forwarded-Proto: http",
+ setupReq: func(r *http.Request) { r.Header.Set("X-Forwarded-Proto", "http") },
+ wantSecure: false,
+ },
+ {
+ name: "TLS takes precedence over contradictory header",
+ setupReq: func(r *http.Request) {
+ r.TLS = &tls.ConnectionState{}
+ r.Header.Set("X-Forwarded-Proto", "http")
+ },
+ wantSecure: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
+ tt.setupReq(r)
+ assert.Equal(t, tt.wantSecure, IsSecure(r))
+ })
+ }
+}
+
+func newTestStore() *sessions.CookieStore {
+ return sessions.NewCookieStore([]byte("test-secret-key"))
+}
+
+func Test_SaveSessionWithSecureCookie_SecureCookieOnHTTPS(t *testing.T) {
+ store := newTestStore()
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
+ r.Header.Set("X-Forwarded-Proto", "https")
+ w := httptest.NewRecorder()
+
+ session, _ := store.Get(r, "session-name")
+ assert.NoError(t, SaveSessionWithSecureCookie(session, r, w))
+ assert.Contains(t, w.Header().Get("Set-Cookie"), "Secure")
+}
+
+func Test_SaveSessionWithSecureCookie_NoSecureCookieOnHTTP(t *testing.T) {
+ store := newTestStore()
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
+ w := httptest.NewRecorder()
+
+ session, _ := store.Get(r, "session-name")
+ assert.NoError(t, SaveSessionWithSecureCookie(session, r, w))
+ assert.NotContains(t, w.Header().Get("Set-Cookie"), "Secure")
+}
+
+func Test_SaveSessionWithSecureCookie_RestoresOriginalSecureFlag(t *testing.T) {
+ tests := []struct {
+ name string
+ originalSecure bool
+ proto string
+ }{
+ {"original false, HTTPS request — restored to false", false, "https"},
+ {"original true, HTTP request — restored to true", true, ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ store := newTestStore()
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
+ if tt.proto != "" {
+ r.Header.Set("X-Forwarded-Proto", tt.proto)
+ }
+ w := httptest.NewRecorder()
+
+ session, _ := store.Get(r, "session-name")
+ session.Options.Secure = tt.originalSecure
+
+ _ = SaveSessionWithSecureCookie(session, r, w)
+
+ assert.Equal(t, tt.originalSecure, session.Options.Secure)
+ })
+ }
+}
From 2e6e36abb920ad913017b2a63ede3fbc4373fae3 Mon Sep 17 00:00:00 2001
From: Vasist
Date: Sat, 6 Jun 2026 00:59:36 +0530
Subject: [PATCH 3/3] fix: date-time-picker test for CI
---
.../ui/__tests__/date-time-picker.test.tsx | 21 ++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/ui/__tests__/date-time-picker.test.tsx b/frontend/src/components/ui/__tests__/date-time-picker.test.tsx
index 73f7feb5..85cddbc7 100644
--- a/frontend/src/components/ui/__tests__/date-time-picker.test.tsx
+++ b/frontend/src/components/ui/__tests__/date-time-picker.test.tsx
@@ -4,6 +4,15 @@ import { DateTimePicker } from '../date-time-picker';
import '@testing-library/jest-dom';
describe('DateTimePicker', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date(2026, 1, 1));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
it('renders without crashing', () => {
const mockOnDateTimeChange = jest.fn();
render(
@@ -18,7 +27,9 @@ describe('DateTimePicker', () => {
});
it('opens and closes the popover when the trigger button is clicked', async () => {
- const user = userEvent.setup();
+ const user = userEvent.setup({
+ advanceTimers: jest.advanceTimersByTime.bind(jest),
+ });
const mockOnDateTimeChange = jest.fn();
render(
{
});
it('allows selecting a date from the calendar', async () => {
- const user = userEvent.setup();
+ const user = userEvent.setup({
+ advanceTimers: jest.advanceTimersByTime.bind(jest),
+ });
const mockOnDateTimeChange = jest.fn();
render(
{
});
it('allows selecting an hour, minute, and AM/PM', async () => {
- const user = userEvent.setup();
+ const user = userEvent.setup({
+ advanceTimers: jest.advanceTimersByTime.bind(jest),
+ });
const mockOnDateTimeChange = jest.fn();
const initialDate = new Date(2024, 0, 15, 10, 30); // Jan 15, 2024, 10:30 AM
render(