Skip to content

Commit b05bfcb

Browse files
committed
fix: resolve session persistence by converting to client-side auth and fix Docker entrypoint line endings
1 parent 4e2c99d commit b05bfcb

File tree

5 files changed

+89
-98
lines changed

5 files changed

+89
-98
lines changed

backend/api/views.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,17 @@ def post(self, request, *args, **kwargs):
5757

5858
if user is not None:
5959
auth_login(request, user)
60-
return JsonResponse({"detail": "Successfully logged in."})
60+
return JsonResponse({
61+
"detail": "Successfully logged in.",
62+
"user": {
63+
"id": user.id,
64+
"username": user.username,
65+
"email": user.email,
66+
"is_staff": user.is_staff,
67+
"is_superuser": user.is_superuser,
68+
},
69+
"token": None # Session-based auth, no token needed
70+
})
6171
else:
6272
return JsonResponse(
6373
{"detail": "Invalid login credentials."},

frontend/Dockerfile.dev

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ COPY package*.json .npmrc ./
88
# Install dependencies with CI-optimized flags
99
RUN npm ci --prefer-offline --no-audit --no-fund
1010

11-
# Copy entrypoint script and make it executable
12-
COPY docker-entrypoint.sh /usr/local/bin/
13-
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
14-
11+
# Copy application files
1512
COPY . .
1613

14+
# Copy entrypoint script, convert Windows line endings to Unix, and make it executable
15+
COPY docker-entrypoint.sh /usr/local/bin/
16+
RUN sed -i 's/\r$//' /usr/local/bin/docker-entrypoint.sh && chmod +x /usr/local/bin/docker-entrypoint.sh
17+
1718
EXPOSE 3000
1819

1920
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

frontend/app/components/Login.tsx

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
11
import * as React from 'react';
22
import type { FC, ChangeEvent } from "react";
33
import { Container, Grid } from '@mui/material';
4-
import { useNavigate, Form, useSubmit, useActionData } from '@remix-run/react';
4+
import { useNavigate } from '@remix-run/react';
55
import { TextField, Typography, Button, CircularProgress } from '@mui/material';
66
import ReCAPTCHA from 'react-google-recaptcha';
77
import type { ReCAPTCHA as ReCAPTCHAType } from 'react-google-recaptcha';
88
import UserContext from '../contexts/UserContext';
99

1010
interface LoginProps {
11-
onLogin: () => void;
11+
onLogin: (username: string, password: string, recaptcha: string) => Promise<{ success: boolean }>;
1212
recaptchaSiteKey: string;
1313
}
1414

15-
export const Login: FC<LoginProps> = ({ recaptchaSiteKey }) => {
15+
export const Login: FC<LoginProps> = ({ recaptchaSiteKey, onLogin }) => {
1616
const [username, setUsername] = React.useState<string>("");
1717
const [password, setPassword] = React.useState<string>("");
1818
const [usernameInvalid, setUsernameInvalid] = React.useState<boolean>(false);
1919
const [passwordInvalid, setPasswordInvalid] = React.useState<boolean>(false);
2020
const [isLoading, setIsLoading] = React.useState<boolean>(false);
2121

2222
const navigate = useNavigate();
23-
const submit = useSubmit();
2423
const recaptchaRef = React.useRef<ReCAPTCHAType>(null);
25-
const formRef = React.useRef<HTMLFormElement>(null);
26-
const actionData = useActionData<{ success?: boolean; error?: string; user?: any }>();
27-
const { updateUser } = React.useContext(UserContext);
28-
29-
React.useEffect(() => {
30-
if (actionData?.success && actionData.user) {
31-
updateUser(actionData.user);
32-
navigate("/");
33-
}
34-
if (actionData?.error) {
35-
setIsLoading(false);
36-
}
37-
}, [actionData, navigate, updateUser]);
24+
const [error, setError] = React.useState<string>("");
3825

3926
const validateForm = (): boolean => {
4027
const isUsernameInvalid = !username;
@@ -65,23 +52,20 @@ export const Login: FC<LoginProps> = ({ recaptchaSiteKey }) => {
6552
return;
6653
}
6754

68-
if (formRef.current) {
69-
const formData = new FormData(formRef.current);
70-
formData.set('username', username);
71-
formData.set('password', password);
72-
formData.set('recaptcha', token);
73-
74-
console.log('Submitting form with data:', {
75-
username,
76-
recaptcha: 'present'
77-
});
78-
79-
submit(formData, {
80-
method: 'post'
81-
});
55+
console.log('Calling onLogin with data:', {
56+
username,
57+
recaptcha: 'present'
58+
});
59+
60+
// Call the client-side login handler
61+
const result = await onLogin(username, password, token);
62+
if (result.success) {
63+
setIsLoading(false);
8264
}
65+
8366
} catch (error) {
84-
console.error('Error during form submission:', error);
67+
console.error('Error during login:', error);
68+
setError(error instanceof Error ? error.message : 'Login failed');
8569
setIsLoading(false);
8670
}
8771
};
@@ -101,12 +85,12 @@ export const Login: FC<LoginProps> = ({ recaptchaSiteKey }) => {
10185
<Grid container justifyContent="center">
10286
<Grid item xs={12} md={8} lg={3}>
10387
<Typography variant="h4" component="h2" sx={{ mb: 3 }}>Login</Typography>
104-
{actionData?.error && (
88+
{error && (
10589
<Typography color="error" sx={{ mb: 2 }}>
106-
{actionData.error}
90+
{error}
10791
</Typography>
10892
)}
109-
<Form ref={formRef} method="post">
93+
<form>
11094
<input type="hidden" name="recaptcha" value="" />
11195
<TextField
11296
id="username"
@@ -168,7 +152,7 @@ export const Login: FC<LoginProps> = ({ recaptchaSiteKey }) => {
168152
>
169153
Forgot Password?
170154
</Button>
171-
</Form>
155+
</form>
172156
</Grid>
173157
</Grid>
174158
</Container>

frontend/app/routes/_app.login.tsx

Lines changed: 27 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,52 +26,7 @@ export async function loader() {
2626
});
2727
}
2828

29-
export async function action({ request }: ActionFunctionArgs) {
30-
const formData = await request.formData();
31-
const username = formData.get("username");
32-
const password = formData.get("password");
33-
const recaptcha = formData.get("recaptcha");
34-
35-
if (typeof username !== "string" || typeof password !== "string" || typeof recaptcha !== "string") {
36-
return json(
37-
{ error: "Invalid form submission" },
38-
{ status: 400 }
39-
);
40-
}
41-
42-
if (!username || !password || !recaptcha) {
43-
return json(
44-
{ error: "All fields are required" },
45-
{ status: 400 }
46-
);
47-
}
48-
49-
try {
50-
const response = await api.post('/auth/login/', {
51-
username,
52-
password,
53-
recaptcha,
54-
});
55-
56-
return json({
57-
success: true,
58-
user: response.data.user,
59-
token: response.data.token
60-
});
61-
62-
} catch (error) {
63-
if (isAxiosError(error) && error.response) {
64-
return json(
65-
{ error: error.response.data.detail || "Login failed" },
66-
{ status: error.response.status }
67-
);
68-
}
69-
return json(
70-
{ error: "Login failed. Please check your credentials." },
71-
{ status: 401 }
72-
);
73-
}
74-
}
29+
// Remove server-side action - login will be handled client-side
7530

7631
export default function LoginPage() {
7732
const data = useLoaderData<typeof loader>();
@@ -87,6 +42,29 @@ export default function LoginPage() {
8742
// Set the navigate function for the API utility
8843
api.setNavigate(navigate);
8944
}, [navigate]);
45+
46+
const handleLogin = async (username: string, password: string, recaptcha: string) => {
47+
try {
48+
const response = await api.post('/auth/login/', {
49+
username,
50+
password,
51+
recaptcha,
52+
});
53+
54+
// Update user context with the logged-in user
55+
updateUser(response.data.user);
56+
57+
// Navigate to home page
58+
navigate('/');
59+
60+
return { success: true };
61+
} catch (error) {
62+
if (isAxiosError(error) && error.response) {
63+
throw new Error(error.response.data.detail || "Login failed");
64+
}
65+
throw new Error("Login failed. Please check your credentials.");
66+
}
67+
};
9068

9169
if (!data.RECAPTCHA_SITE_KEY) {
9270
return (
@@ -108,12 +86,9 @@ export default function LoginPage() {
10886
}
10987

11088
return (
111-
<Login
112-
recaptchaSiteKey={data.RECAPTCHA_SITE_KEY}
113-
onLogin={() => {
114-
// The Login component will call this without arguments
115-
// but we'll update the user context when we get the response in the action
116-
}}
89+
<Login
90+
recaptchaSiteKey={data.RECAPTCHA_SITE_KEY}
91+
onLogin={handleLogin}
11792
/>
11893
);
11994
}

frontend/app/routes/_app.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ export interface LoaderData {
1919

2020
export const loader: LoaderFunction = async ({ request }) => {
2121
try {
22-
const [siteData, user] = await Promise.all([
23-
fetchSiteTitle(),
24-
fetchAuthenticatedUser(request),
25-
]);
26-
return json<LoaderData>({ siteData, user });
22+
const siteData = await fetchSiteTitle();
23+
// Don't try to fetch user on server-side due to cookie forwarding issues
24+
// User will be fetched client-side after hydration
25+
return json<LoaderData>({ siteData, user: null });
2726
} catch (error) {
2827
return json<LoaderData>({
2928
siteData: { site_name: "Our Platform" },
@@ -45,6 +44,28 @@ export default function AppLayout() {
4544
// Use cached loader data instead of regular useLoaderData
4645
const { siteData, user } = useCachedLoaderData<LoaderData>();
4746
const [currentUser, setUser] = React.useState<User | null>(user);
47+
const [isLoading, setIsLoading] = React.useState(true);
48+
49+
// Fetch user client-side after hydration to avoid cookie forwarding issues
50+
React.useEffect(() => {
51+
const fetchUser = async () => {
52+
try {
53+
const response = await fetch('http://localhost/api/users/me/', {
54+
credentials: 'include',
55+
});
56+
if (response.ok) {
57+
const userData = await response.json();
58+
setUser(userData);
59+
}
60+
} catch (error) {
61+
console.log('User not authenticated or error fetching user:', error);
62+
} finally {
63+
setIsLoading(false);
64+
}
65+
};
66+
67+
fetchUser();
68+
}, []);
4869

4970
const userContextValue: UserContextType = {
5071
user: currentUser,

0 commit comments

Comments
 (0)