Skip to content

Commit 5790da7

Browse files
committed
added logouts for patient & staff
1 parent 85aeef0 commit 5790da7

11 files changed

Lines changed: 290 additions & 78 deletions

File tree

api-gateway/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'dotenv/config';
22
import app from './app.js';
33
import logger from './logger.js';
44

5-
const PORT = process.env.PORT || 3000;
5+
const PORT = process.env.PORT;
66

77
app.listen(PORT, () => {
88
logger.info(`🚀 API Gateway running on port ${PORT}`);

frontend/src/app/dashboard/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
55
import { useEffect } from 'react';
66

77
export default function DashboardPage() {
8-
const { auth } = useAuth();
8+
const { auth, logout } = useAuth();
99
const router = useRouter();
1010

1111
useEffect(() => {
@@ -29,6 +29,9 @@ if (auth.isRestoring) {
2929

3030
return (
3131
<div className="min-h-screen p-8">
32+
<div>
33+
<button className="bg-red-500 text-1xl text-white font-bold p-2 fixed left-[30vh] top-50" onClick={logout}>logout</button>
34+
</div>
3235
<h1 className="text-2xl font-semibold mb-4">
3336
Patient Dashboard
3437
</h1>

frontend/src/app/staff/dashboard/page.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
55
import { useEffect } from 'react';
66

77
export default function StaffDashboardPage() {
8-
const { auth } = useStaffAuth();
8+
const { auth, logout } = useStaffAuth();
99
const router = useRouter();
1010

1111
useEffect(() => {
@@ -14,18 +14,29 @@ export default function StaffDashboardPage() {
1414
}
1515
}, [auth.accessToken, auth.isRestoring, router]);
1616

17-
if (auth.isRestoring) {
18-
return (
19-
<div className="min-h-screen flex items-center justify-center">
20-
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
21-
</div>
22-
);
23-
}
17+
if (auth.isRestoring) {
18+
return (
19+
<div className="min-h-screen flex items-center justify-center">
20+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
21+
</div>
22+
);
23+
}
2424

2525
if (!auth.accessToken) return null;
2626

2727
return (
2828
<div className="min-h-screen p-8 bg-gray-50">
29+
30+
{/* 🔥 LOGOUT BUTTON */}
31+
<div className="flex justify-end mb-6">
32+
<button
33+
onClick={logout}
34+
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 transition"
35+
>
36+
Logout
37+
</button>
38+
</div>
39+
2940
<h1 className="text-2xl font-semibold mb-6">
3041
Staff Dashboard
3142
</h1>

frontend/src/auth/auth.provider.tsx

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { Patient } from './auth.types';
1212
import { refreshPatient } from './auth.service';
1313
import { api } from '../lib/api';
14+
import { logoutPatient } from './auth.service';
1415

1516
type AuthState = {
1617
accessToken: string | null;
@@ -109,48 +110,41 @@ export function AuthProvider({ children }: { children: ReactNode }) {
109110

110111
// 🔁 Restore session on page load
111112
// 🔁 Restore session ONLY if no accessToken
113+
// 🔁 Restore session ONCE on mount
112114
useEffect(() => {
113-
if (auth.accessToken) {
114-
setAuth(prev => ({ ...prev, isRestoring: false }));
115-
return;
116-
}
117-
118115
let cancelled = false;
119116

120-
async function restoreSession() {
121-
try {
122-
const refreshRes = await refreshPatient();
123-
124-
accessTokenRef.current = refreshRes.accessToken;
117+
(async () => {
118+
try {
119+
const refreshRes = await refreshPatient();
125120

126-
const profileRes = await api.get('/patients/public/auth/me');
121+
accessTokenRef.current = refreshRes.accessToken;
127122

128-
if (cancelled) return;
123+
const profileRes = await api.get('/patients/public/auth/me');
129124

130-
setAuth({
131-
accessToken: refreshRes.accessToken,
132-
patient: profileRes.data,
133-
isRestoring: false,
134-
});
135-
136-
} catch {
137-
if (cancelled) return;
138-
139-
setAuth({
140-
accessToken: null,
141-
patient: null,
142-
isRestoring: false,
143-
});
144-
}
145-
}
125+
if (cancelled) return;
146126

127+
setAuth({
128+
accessToken: refreshRes.accessToken,
129+
patient: profileRes.data,
130+
isRestoring: false,
131+
});
132+
} catch {
133+
if (cancelled) return;
147134

148-
restoreSession();
135+
setAuth({
136+
accessToken: null,
137+
patient: null,
138+
isRestoring: false,
139+
});
140+
}
141+
})();
149142

150143
return () => {
151144
cancelled = true;
152145
};
153-
}, [auth.accessToken]);
146+
}, []); // ⚠️ EMPTY DEP ARRAY
147+
154148

155149

156150
function loginSuccess(data: {
@@ -164,13 +158,18 @@ async function restoreSession() {
164158
});
165159
}
166160

167-
function logout() {
168-
setAuth({
169-
accessToken: null,
170-
patient: null,
171-
isRestoring: false,
172-
});
173-
}
161+
async function logout() {
162+
try {
163+
await logoutPatient();
164+
} catch {}
165+
166+
setAuth({
167+
accessToken: null,
168+
patient: null,
169+
isRestoring: false,
170+
});
171+
}
172+
174173

175174
return (
176175
<AuthContext.Provider value={{ auth, loginSuccess, logout }}>

frontend/src/auth/auth.service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import axios, { AxiosError } from 'axios';
12
import { api } from '../lib/api';
23
import { LoginResponseSchema } from './auth.types';
3-
44
export async function loginPatient(email: string, password: string) {
55
const res = await api.post('/patients/public/auth/login', {
66
email,
@@ -24,8 +24,12 @@ export async function registerPatient(data: {
2424
try {
2525
const res = await api.post('/patients/public/auth/register', data);
2626
return res.data;
27-
} catch (err: any) {
28-
console.error('REGISTER ERROR RESPONSE:', err.response?.data);
27+
} catch (err: unknown) {
28+
if (axios.isAxiosError(err)) {
29+
console.error('REGISTER ERROR RESPONSE:', err.response?.data);
30+
} else {
31+
console.error('REGISTER ERROR:', err);
32+
}
2933
throw err;
3034
}
3135
}
@@ -34,3 +38,7 @@ export async function refreshPatient() {
3438
const res = await api.post('/patients/public/auth/refresh');
3539
return res.data as { accessToken: string };
3640
}
41+
42+
export async function logoutPatient() {
43+
await api.post('/patients/public/auth/logout');
44+
}

frontend/src/staff/auth/staff.auth.provider.tsx

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,16 @@ type StaffAuthState = {
1818
isRestoring: boolean;
1919
};
2020

21-
const StaffAuthContext = createContext<any>(null);
21+
type StaffAuthContextType = {
22+
auth: StaffAuthState;
23+
loginSuccess: (data: {
24+
accessToken: string;
25+
staff: Staff;
26+
}) => void;
27+
logout: () => Promise<void>;
28+
};
29+
30+
const StaffAuthContext = createContext<StaffAuthContextType | null>(null);
2231

2332
export function StaffAuthProvider({ children }: { children: ReactNode }) {
2433
const [auth, setAuth] = useState<StaffAuthState>({
@@ -28,11 +37,15 @@ export function StaffAuthProvider({ children }: { children: ReactNode }) {
2837
});
2938

3039
const tokenRef = useRef<string | null>(null);
40+
const refreshingRef = useRef<Promise<string> | null>(null);
3141

3242
useEffect(() => {
3343
tokenRef.current = auth.accessToken;
3444
}, [auth.accessToken]);
3545

46+
/**
47+
* ✅ Axios interceptor
48+
*/
3649
useEffect(() => {
3750
const reqId = api.interceptors.request.use(config => {
3851
if (tokenRef.current) {
@@ -41,52 +54,131 @@ export function StaffAuthProvider({ children }: { children: ReactNode }) {
4154
return config;
4255
});
4356

57+
const resId = api.interceptors.response.use(
58+
res => res,
59+
async error => {
60+
const original = error.config;
61+
62+
if (
63+
error.response?.status !== 401 ||
64+
original?._retry ||
65+
original?.url?.includes('/staff/public/auth/login') ||
66+
original?.url?.includes('/staff/public/auth/refresh')
67+
) {
68+
return Promise.reject(error);
69+
}
70+
71+
original._retry = true;
72+
73+
try {
74+
if (!refreshingRef.current) {
75+
refreshingRef.current = refreshStaff().then(res => {
76+
tokenRef.current = res.accessToken;
77+
setAuth(prev => ({
78+
...prev,
79+
accessToken: res.accessToken,
80+
}));
81+
refreshingRef.current = null;
82+
return res.accessToken;
83+
});
84+
}
85+
86+
const newToken = await refreshingRef.current;
87+
original.headers.Authorization = `Bearer ${newToken}`;
88+
return api(original);
89+
} catch {
90+
refreshingRef.current = null;
91+
setAuth({
92+
accessToken: null,
93+
staff: null,
94+
isRestoring: false,
95+
});
96+
return Promise.reject(error);
97+
}
98+
}
99+
);
100+
44101
return () => {
45102
api.interceptors.request.eject(reqId);
103+
api.interceptors.response.eject(resId);
46104
};
47105
}, []);
48106

107+
/**
108+
* ✅ Restore session ONCE on mount
109+
*/
49110
useEffect(() => {
50-
async function restore() {
111+
let cancelled = false;
112+
113+
(async () => {
51114
try {
52115
const refreshRes = await refreshStaff();
53116

54117
tokenRef.current = refreshRes.accessToken;
55118

56119
const profile = await api.get('/staff/public/auth/me');
57120

121+
if (cancelled) return;
122+
58123
setAuth({
59124
accessToken: refreshRes.accessToken,
60125
staff: profile.data,
61126
isRestoring: false,
62127
});
63128
} catch {
129+
if (cancelled) return;
130+
64131
setAuth({
65132
accessToken: null,
66133
staff: null,
67134
isRestoring: false,
68135
});
69136
}
70-
}
137+
})();
71138

72-
restore();
139+
return () => {
140+
cancelled = true;
141+
};
73142
}, []);
74143

75-
function loginSuccess(data: any) {
144+
function loginSuccess(data: {
145+
accessToken: string;
146+
staff: Staff;
147+
}) {
148+
tokenRef.current = data.accessToken;
149+
76150
setAuth({
77151
accessToken: data.accessToken,
78152
staff: data.staff,
79153
isRestoring: false,
80154
});
81155
}
82156

157+
async function logout() {
158+
try {
159+
await api.post('/staff/public/auth/logout');
160+
} catch {}
161+
162+
tokenRef.current = null;
163+
164+
setAuth({
165+
accessToken: null,
166+
staff: null,
167+
isRestoring: false,
168+
});
169+
}
170+
83171
return (
84-
<StaffAuthContext.Provider value={{ auth, loginSuccess }}>
172+
<StaffAuthContext.Provider value={{ auth, loginSuccess, logout }}>
85173
{children}
86174
</StaffAuthContext.Provider>
87175
);
88176
}
89177

90178
export function useStaffAuth() {
91-
return useContext(StaffAuthContext);
179+
const ctx = useContext(StaffAuthContext);
180+
if (!ctx) {
181+
throw new Error('useStaffAuth must be used inside StaffAuthProvider');
182+
}
183+
return ctx;
92184
}

0 commit comments

Comments
 (0)