Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import {
V1_API_ENDPOINTS,
CONVERSATION_ENDPOINTS,
AUTH_ENDPOINTS,
endpoints,
} from "./endpoints";

Expand Down Expand Up @@ -31,6 +32,70 @@
(error) => Promise.reject(error),
);

// Response interceptor to handle token refresh on 401
let isRefreshing = false;
let failedQueue: { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }[] = [];

const processQueue = (error: unknown, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};

adminApi.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `JWT ${token}`;
return adminApi(originalRequest);
}).catch((err) => Promise.reject(err));
}

originalRequest._retry = true;
isRefreshing = true;

const refreshToken = localStorage.getItem("refresh");

if (!refreshToken) {
localStorage.removeItem("access");
localStorage.removeItem("refresh");
window.location.href = "/login";
return Promise.reject(error);
}

try {
const response = await axios.post(AUTH_ENDPOINTS.JWT_REFRESH, { refresh: refreshToken });
const newAccessToken = response.data.access;
localStorage.setItem("access", newAccessToken);
processQueue(null, newAccessToken);
originalRequest.headers.Authorization = `JWT ${newAccessToken}`;
return adminApi(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem("access");
localStorage.removeItem("refresh");
window.location.href = "/login";
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}

return Promise.reject(error);
},
);

const handleSubmitFeedback = async (
feedbackType: FormValues["feedbackType"],
name: FormValues["name"],
Expand Down Expand Up @@ -97,7 +162,7 @@

interface StreamCallbacks {
onContent?: (content: string) => void;
onComplete?: (data: { embeddings_info: any[]; done: boolean }) => void;

Check warning on line 165 in frontend/src/api/apiClient.ts

View workflow job for this annotation

GitHub Actions / Lint and Build

Unexpected any. Specify a different type
onError?: (error: string) => void;
onMetadata?: (data: { question: string; embeddings_count: number }) => void;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const AUTH_ENDPOINTS = {
USERS_CREATE: `${API_BASE}/auth/users/`,
USERS_ACTIVATION: `${API_BASE}/auth/users/activation/`,
USERS_RESEND_ACTIVATION: `${API_BASE}/auth/users/resend_activation/`,
JWT_REFRESH: `${API_BASE}/auth/jwt/refresh/`,
} as const;

/**
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,14 @@ const Header: React.FC<LoginFormProps> = ({ isAuthenticated, isSuperuser }) => {
Balancer
</span>
<Chat showChat={showChat} setShowChat={setShowChat} />
{isAuthenticated && authLinks()}
{isAuthenticated ? authLinks() : (
<Link
to="/login"
className="font-satoshi flex cursor-pointer items-center text-black hover:text-blue-600"
>
Log In
</Link>
)}
</div>
<MdNavBar handleForm={handleForm} isAuthenticated={isAuthenticated} />
</header>
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/components/Header/MdNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,25 @@ const MdNavBar = (props: LoginFormProps) => {
Support Development
</a>
</li>
{isAuthenticated &&
{isAuthenticated ? (
<li className="border-b border-gray-300 p-4">
<Link
to="/logout"
className="mr-9 text-black hover:border-b-2 hover:border-blue-600 hover:text-black hover:no-underline"
>Sign Out
to="/logout"
className="mr-9 text-black hover:border-b-2 hover:border-blue-600 hover:text-black hover:no-underline"
>
Sign Out
</Link>
</li>
) : (
<li className="border-b border-gray-300 p-4">
<Link
to="/login"
className="mr-9 text-black hover:border-b-2 hover:border-blue-600 hover:text-black hover:no-underline"
>
Log In
</Link>
</li>
}
)}
</ul>
</div>
<Chat showChat={showChat} setShowChat={setShowChat}/>
Expand Down
32 changes: 10 additions & 22 deletions frontend/src/pages/Login/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { RootState } from "../../services/actions/types";
import { useState, useEffect } from "react";
import ErrorMessage from "../../components/ErrorMessage";
import LoadingSpinner from "../../components/LoadingSpinner/LoadingSpinner";
import { FaExclamationTriangle } from "react-icons/fa";

interface LoginFormProps {
isAuthenticated: boolean | null;
Expand Down Expand Up @@ -60,19 +59,9 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) {
className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12"
>
<div className="flex flex-col items-center justify-center">
{/* {errorMessage && <div className="text-red-500">{errorMessage}</div>} */}
<h2 className="blue_gradient mb-6 font-satoshi text-3xl font-bold text-gray-600">
Welcome
Log in
</h2>

<blockquote className="p-4 mb-4 border-s-4 border-yellow-500 bg-amber-50 flex gap-5 items-center">
<div className="mb-2 text-yellow-500">
<FaExclamationTriangle size={24} />
</div>
<div>
<p className="text-gray-800">This login is for Code for Philly administrators. Providers can use all site features without logging in. <Link to="/" className="underline hover:text-blue-600 hover:no-underline" style={{ 'whiteSpace': 'nowrap' }}>Return to Homepage</Link></p>
</div>
</blockquote>
</div>
<ErrorMessage errors={errors} />
<div className="mb-4 mt-5">
Expand Down Expand Up @@ -113,18 +102,17 @@ function LoginForm({ isAuthenticated, loginError }: LoginFormProps) {
Sign In
</button>
</div>
<div className="mt-4 flex justify-between text-sm">
<Link to="/register" className="text-blue-600 hover:underline">
Don't have an account? Sign up
</Link>
<Link to="/resetPassword" className="text-blue-600 hover:underline">
Forgot password?
</Link>
</div>
</form>
</section>
{ loading && <LoadingSpinner /> }

{/* <p>
Don't have an account?{" "}
<Link to="/register" className="font-bold hover:text-blue-600">
{" "}
Register here
</Link>
.
</p> */}
{ loading && <LoadingSpinner /> }
</>
);
}
Expand Down
112 changes: 75 additions & 37 deletions frontend/src/pages/Login/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useFormik } from "formik";
import { useNavigate } from "react-router-dom";
import { useNavigate, Link } from "react-router-dom";
import { reset_password, AppDispatch } from "../../services/actions/auth";
import { connect, useDispatch } from "react-redux";
import { RootState } from "../../services/actions/types";
import { useEffect, useState } from "react";
import axios from "axios";
import { AUTH_ENDPOINTS } from "../../api/endpoints";
import Layout from "../Layout/Layout";

interface ResetPasswordProps {
Expand All @@ -14,6 +16,8 @@ function ResetPassword(props: ResetPasswordProps) {
const { isAuthenticated } = props;
const dispatch = useDispatch<AppDispatch>();
const [requestSent, setRequestSent] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState("");
const [resendStatus, setResendStatus] = useState<"idle" | "sent" | "error">("idle");

const navigate = useNavigate();

Expand All @@ -29,58 +33,92 @@ function ResetPassword(props: ResetPasswordProps) {
},
onSubmit: (values) => {
dispatch(reset_password(values.email));
setSubmittedEmail(values.email);
setRequestSent(true);
},
});

const handleResend = async () => {
try {
await axios.post(AUTH_ENDPOINTS.RESET_PASSWORD, { email: submittedEmail });
setResendStatus("sent");
} catch {
setResendStatus("error");
}
};

if (requestSent) {
navigate("/");
}
return (
<>
return (
<Layout>
<section className="mx-auto mt-36 w-full max-w-xs">
<h2 className="blue_gradient mb-6 font-satoshi text-xl font-bold text-gray-600">
Reset Password
</h2>
<form
onSubmit={handleSubmit}
className="mb-4 rounded bg-white px-8 pb-8 pt-6 shadow-md"
>
<div className="mb-4">
<label
htmlFor="email"
className="mb-2 block text-sm font-bold text-gray-700"
>
Email
</label>
<input
id="login-email"
name="email"
type="email"
onChange={handleChange}
value={values.email}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
/>
</div>
<div className="flex items-center justify-between">
<button className="black_btn" type="submit">
Reset Password
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem] text-center">
<div className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12">
<h2 className="blue_gradient mb-4 font-satoshi text-3xl font-bold text-gray-600">
Check your email
</h2>
<p className="text-gray-600 mb-6">
If an account exists for <strong>{submittedEmail}</strong>, you'll receive a password reset link shortly.
</p>
<div className="flex flex-col gap-3">
<Link to="/login" className="btnBlue w-full text-lg text-center block">
Back to log in
</Link>
<button onClick={handleResend} className="text-sm text-blue-600 hover:underline" type="button">
{resendStatus === "sent"
? "Email resent!"
: resendStatus === "error"
? "Failed to resend. Try again."
: "Resend email"}
</button>
</div>
</form>
</div>
</section>
</Layout>
</>
);
}

return (
<Layout>
<section className="mx-auto mt-24 w-[20rem] md:mt-48 md:w-[32rem]">
<form
onSubmit={handleSubmit}
className="mb-4 rounded-md bg-white px-3 pb-12 pt-6 shadow-md ring-1 md:px-12"
>
<h2 className="blue_gradient mb-6 font-satoshi text-3xl font-bold text-gray-600 text-center">
Reset password
</h2>
<div className="mb-4">
<label
htmlFor="email"
className="mb-2 block text-lg font-bold text-gray-700"
>
Email
</label>
<input
id="login-email"
name="email"
type="email"
onChange={handleChange}
value={values.email}
className="focus:shadow-outline w-full appearance-none rounded border px-3 py-3 leading-tight text-gray-700 shadow focus:outline-none"
/>
</div>
<button className="btnBlue w-full text-lg" type="submit">
Send reset link
</button>
<div className="mt-4 text-center">
<Link to="/login" className="text-sm text-blue-600 hover:underline">
Back to log in
</Link>
</div>
</form>
</section>
</Layout>
);
}

const mapStateToProps = (state: RootState) => ({
isAuthenticated: state.auth.isAuthenticated,
});

// Assign the connected component to a named constant
const ConnectedResetPassword = connect(mapStateToProps)(ResetPassword);

// Export the named constant
export default ConnectedResetPassword;
Loading
Loading