Skip to content

Commit f4bf5b5

Browse files
committed
Implement session management in the admin application by introducing session check API endpoint and updating authentication state handling. Refactor App component to manage authentication state with improved error handling and loading indicators.
1 parent 57e64bf commit f4bf5b5

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

‎admin/src/main.tsx‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import "./index.css";
1919
import { useListPageSize } from "./useListPageSize";
2020
import { AuditSection } from "./AuditSection";
2121
import { SettingsSection } from "./SettingsSection";
22-
import { api, withMinRefreshDuration } from "./lib/api";
22+
import { api, ApiError, withMinRefreshDuration } from "./lib/api";
2323
import {
2424
Button,
2525
EmptyState,
@@ -55,9 +55,12 @@ type Notice = {
5555

5656
type AppSection = "bots" | "audit" | "settings";
5757

58+
type AuthState = "checking" | "signed-out" | "signed-in";
59+
5860
function App() {
5961
const reduceMotion = useReducedMotion();
60-
const [isSignedIn, setIsSignedIn] = useState(false);
62+
const [authState, setAuthState] = useState<AuthState>("checking");
63+
const isSignedIn = authState === "signed-in";
6164
const [password, setPassword] = useState("");
6265
const [label, setLabel] = useState("");
6366
const [token, setToken] = useState("");
@@ -100,20 +103,43 @@ function App() {
100103
const data = await api<BotsResponse>("/api/bots");
101104
setBots(data.bots);
102105
});
106+
} catch (error) {
107+
if (error instanceof ApiError && error.status === 401) {
108+
setAuthState("signed-out");
109+
return;
110+
}
111+
setNotice({ kind: "error", text: "Refresh failed." });
103112
} finally {
104113
setIsRefreshingBots(false);
105114
}
106115
}, []);
107116

117+
useEffect(() => {
118+
let cancelled = false;
119+
120+
void api<void>("/api/session")
121+
.then(() => {
122+
if (!cancelled) {
123+
setAuthState("signed-in");
124+
}
125+
})
126+
.catch(() => {
127+
if (!cancelled) {
128+
setAuthState("signed-out");
129+
}
130+
});
131+
132+
return () => {
133+
cancelled = true;
134+
};
135+
}, []);
108136

109137
useEffect(() => {
110138
if (!isSignedIn) {
111139
return;
112140
}
113141

114-
void loadBots().catch(() => {
115-
setNotice({ kind: "error", text: "Refresh failed." });
116-
});
142+
void loadBots();
117143
}, [isSignedIn, loadBots]);
118144

119145

@@ -128,7 +154,7 @@ function App() {
128154
body: JSON.stringify({ password }),
129155
});
130156
setPassword("");
131-
setIsSignedIn(true);
157+
setAuthState("signed-in");
132158
setNotice({ kind: "idle", text: "" });
133159
} catch {
134160
setNotice({ kind: "error", text: "Wrong password." });
@@ -180,9 +206,17 @@ function App() {
180206

181207
const pageTransition = reduceMotion ? { duration: 0 } : { duration: 0.4, ease: [0.25, 0.1, 0.25, 1] as const };
182208

209+
if (authState === "checking") {
210+
return (
211+
<motion.div className="flex min-h-dvh items-center justify-center p-6" aria-busy aria-label="Checking session">
212+
<Loader2 size={28} className="animate-spin text-zinc-500" aria-hidden />
213+
</motion.div>
214+
);
215+
}
216+
183217
return (
184218
<AnimatePresence mode="wait">
185-
{!isSignedIn ? (
219+
{authState === "signed-out" ? (
186220
<motion.div
187221
key="login"
188222
className="flex min-h-dvh items-center justify-center p-6"

‎src/app.rs‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ pub fn router(state: Arc<AppState>) -> Router {
101101
.route("/admin", get(admin_page))
102102
.route("/healthz", get(healthz))
103103
.route("/api/login", post(login))
104+
.route("/api/session", get(session_check))
104105
.route("/api/bots", get(list_bots).post(register_bot))
105106
.route("/api/bots/{token_hash}", delete(delete_bot))
106107
.route("/api/settings", get(get_settings).put(update_settings))
@@ -316,6 +317,14 @@ fn with_security_headers(mut response: Response) -> Response {
316317
response
317318
}
318319

320+
async fn session_check(State(state): State<Arc<AppState>>, headers: HeaderMap) -> impl IntoResponse {
321+
if is_authorized(&state, &headers) {
322+
StatusCode::NO_CONTENT.into_response()
323+
} else {
324+
StatusCode::UNAUTHORIZED.into_response()
325+
}
326+
}
327+
319328
async fn login(
320329
State(state): State<Arc<AppState>>,
321330
Json(payload): Json<LoginRequest>,
@@ -638,6 +647,25 @@ mod tests {
638647
assert!(response.headers().get(header::SET_COOKIE).is_some());
639648
}
640649

650+
#[tokio::test]
651+
async fn session_check_accepts_valid_cookie() {
652+
let state = test_state();
653+
let token = state.auth.read().unwrap().session_token().to_string();
654+
655+
let response = router(state)
656+
.oneshot(
657+
Request::builder()
658+
.uri("/api/session")
659+
.header(header::COOKIE, format!("{SESSION_COOKIE}={token}"))
660+
.body(Body::empty())
661+
.unwrap(),
662+
)
663+
.await
664+
.unwrap();
665+
666+
assert_eq!(response.status(), StatusCode::NO_CONTENT);
667+
}
668+
641669
#[tokio::test]
642670
async fn rejects_admin_api_without_session() {
643671
let response = router(test_state())

0 commit comments

Comments
 (0)