Skip to content

Commit 6e3fd35

Browse files
chore: improve auth
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 06d8cae commit 6e3fd35

13 files changed

Lines changed: 127 additions & 54 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async-compression={version="0.4", default-features=false, features=["gzip", "tok
3232
tokio-tar={package="astral-tokio-tar", version="0.6"}
3333
blake3={version="1.8"}
3434
argon2={version="0.6.0-rc.8", features=[]}
35-
password-hash={version="0.6.0", features=[
35+
password-hash={version="0.6.1", features=[
3636
"rand_core",
3737
"getrandom",
3838
]} # required for getrandom feature

src/web/session.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use aide::OperationInput;
44
use axum::{
55
extract::FromRequestParts,
66
http::{StatusCode, request::Parts},
7+
response::{IntoResponse, Response},
78
};
89
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
910

@@ -48,25 +49,51 @@ impl OperationInput for SessionUser {}
4849
impl<T> OperationInput for MaybeExtract<T> {}
4950

5051
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for SessionId {
51-
type Rejection = StatusCode;
52+
type Rejection = Response;
5253

5354
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
5455
let jar = CookieJar::from_headers(&parts.headers);
55-
jar.get(SESSION_COOKIE_NAME).map(|c| SessionId(c.value().to_string())).ok_or(StatusCode::UNAUTHORIZED)
56+
57+
let session_cookie = jar.get(SESSION_COOKIE_NAME);
58+
let username_cookie = jar.get(PUBLIC_COOKIE_NAME);
59+
60+
if username_cookie.is_some() && session_cookie.is_none() {
61+
let mut cookie = PUBLIC_COOKIE.clone();
62+
cookie.make_removal();
63+
return Err(Response::builder()
64+
.header("Set-Cookie", cookie.encoded().to_string())
65+
.status(StatusCode::UNAUTHORIZED)
66+
.body("Session expired".into())
67+
.unwrap());
68+
}
69+
70+
if session_cookie.is_some() && username_cookie.is_none() {
71+
let mut cookie = SESSION_COOKIE.clone();
72+
cookie.make_removal();
73+
return Err(Response::builder()
74+
.header("Set-Cookie", cookie.encoded().to_string())
75+
.status(StatusCode::UNAUTHORIZED)
76+
.body("Invalid session".into())
77+
.unwrap());
78+
}
79+
80+
jar.get(SESSION_COOKIE_NAME)
81+
.map(|c| SessionId(c.value().to_string()))
82+
.ok_or(StatusCode::UNAUTHORIZED.into_response())
5683
}
5784
}
5885

5986
impl axum::extract::FromRequestParts<RouterState> for SessionUser {
60-
type Rejection = StatusCode;
87+
type Rejection = Response;
6188

6289
async fn from_request_parts(parts: &mut Parts, state: &RouterState) -> Result<Self, Self::Rejection> {
6390
let session_id = SessionId::from_request_parts(parts, state).await?.0;
6491
let user = state
6592
.app
6693
.sessions
6794
.get(&session_id)
68-
.map_err(|_| StatusCode::UNAUTHORIZED)?
69-
.ok_or(StatusCode::UNAUTHORIZED)?;
95+
.map_err(|_| StatusCode::UNAUTHORIZED.into_response())?
96+
.ok_or(StatusCode::UNAUTHORIZED.into_response())?;
7097

7198
Ok(SessionUser(user))
7299
}

web/bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@types/react-dom": "^19.2.3",
5353
"@types/topojson-client": "^3.1.5",
5454
"@types/topojson-specification": "^1.0.5",
55-
"astro": "6.1.4",
55+
"astro": "6.1.5",
5656
"babel-plugin-react-compiler": "^1.0.0",
5757
"rollup-plugin-license": "^3.7.0",
5858
"typescript": "^6.0.2"

web/src/api/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from "./query";
22
export * from "./constants";
33
export * from "./types";
4-
export * from "./hooks";
4+
export * from "../hooks/api";
55
export * from "./client";

web/src/components/project.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const Project = () => {
6666
setProjectId(window?.document.location.pathname.split("/").pop());
6767
}, []);
6868

69-
const { project } = useProject(projectId);
69+
const { project, notFound } = useProject(projectId);
7070
const {
7171
graph,
7272
isUpdating: graphUpdating,
@@ -99,6 +99,10 @@ export const Project = () => {
9999
[toggleFilter],
100100
);
101101

102+
if (notFound) {
103+
return <div className={styles.notFound}>Project not found</div>;
104+
}
105+
102106
if (!project) return null;
103107

104108
return (
@@ -131,7 +135,7 @@ export const Project = () => {
131135
/>
132136
<GeoCard query={query} onSelect={onSelectDimRow} />
133137
<DimensionTabsCard dimensions={["platform", "browser"]} query={query} onSelect={onSelectDimRow} />
134-
<DimensionTabsCard
138+
<DimensionDropdownCard
135139
dimensions={["mobile", "screen_width", "orientation"]}
136140
query={query}
137141
onSelect={(v) => onSelectDimRow(v, "mobile")}

web/src/components/settings/me.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { User2Icon } from "lucide-react";
2-
import { useEffect, useId, useRef, useState } from "react";
2+
import { useId, useRef } from "react";
33
import { api, useMe, useMutation } from "../../api";
44
import { createToast } from "../toast";
55
import styles from "./me.module.css";
@@ -9,7 +9,8 @@ export const MyAccount = () => {
99
const confirmPasswordId = useId();
1010

1111
const formRef = useRef<HTMLFormElement>(null);
12-
const { role, username, isLoading } = useMe();
12+
const { role, username, isLoading, authError } = useMe();
13+
1314
const { mutate } = useMutation({
1415
mutationFn: api["/api/dashboard/user/{username}/password"].put,
1516
onSuccess: () => {
@@ -19,9 +20,11 @@ export const MyAccount = () => {
1920
onError: console.error,
2021
});
2122

22-
const [loading, setLoading] = useState(true);
23-
useEffect(() => setLoading(isLoading), [isLoading]);
24-
if (loading || !username) return <div className={"loading-spinner"} />;
23+
if (authError) {
24+
return "You don't have permission to view this page.";
25+
}
26+
27+
if (isLoading || !username) return <div className={"loading-spinner"} />;
2528

2629
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
2730
e.preventDefault();

web/src/components/settings/tables.tsx

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Fragment, type ReactElement, useEffect, useRef, useState } from "react";
1+
import { Fragment, type ReactElement, useRef } from "react";
22
import styles from "./tables.module.css";
33

44
import { EditIcon, EllipsisVerticalIcon, RectangleEllipsisIcon, TrashIcon } from "lucide-react";
@@ -14,7 +14,7 @@ import {
1414
useProjects,
1515
useUsers,
1616
} from "../../api";
17-
import { cls } from "../../utils";
17+
import { cls, getUsername } from "../../utils";
1818
import { createToast } from "../toast";
1919

2020
type DropdownOptions = Record<string, ((close: () => void) => ReactElement) | null>;
@@ -74,6 +74,7 @@ const ProjectDropdown = ({ project }: { project: ProjectResponse }) => {
7474

7575
export const ProjectsTable = () => {
7676
const { projects, isLoading } = useProjects();
77+
7778
const columns: Column<(typeof projects)[number]>[] = [
7879
{
7980
id: "displayName",
@@ -164,9 +165,9 @@ const EntityDropdown = ({ entity }: { entity: EntityResponse }) => {
164165
};
165166

166167
export const EntitiesTable = () => {
167-
const { entities, isLoading } = useEntities();
168+
const { entities, isLoading, authError } = useEntities();
168169

169-
if (!useAdminPerms()) {
170+
if (authError) {
170171
return "You don't have permission to view this page.";
171172
}
172173

@@ -211,7 +212,7 @@ export const EntitiesTable = () => {
211212
};
212213

213214
const UserDropdown = ({ user }: { user: UserResponse }) => {
214-
const { username } = useMe();
215+
const username = getUsername();
215216
const options: DropdownOptions = {
216217
edit:
217218
username !== user.username
@@ -256,20 +257,12 @@ const UserDropdown = ({ user }: { user: UserResponse }) => {
256257
return <Dropdown options={options} />;
257258
};
258259

259-
const useAdminPerms = () => {
260-
const { role } = useMe();
261-
const [isMounted, setIsMounted] = useState(false);
262-
useEffect(() => {
263-
setIsMounted(true);
264-
});
265-
return !isMounted || role === "admin";
266-
};
267-
268260
export const UsersTable = () => {
269-
const { users, isLoading } = useUsers();
261+
const { users, isLoading, authError } = useUsers();
270262
const rows = users.map((user) => ({ id: user.username, ...user })) ?? [];
271263

272-
if (!useAdminPerms()) {
264+
// TODO: if the user isn't an admin, show no perms
265+
if (authError) {
273266
return "You don't have permission to view this page.";
274267
}
275268

web/src/components/userInfo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import styles from "./userInfo.module.css";
22

33
import { HelpCircle, LogOutIcon, SettingsIcon, SquareArrowOutUpRightIcon, UserIcon } from "lucide-react";
4-
import { api } from "../api";
4+
import { api, queryClient } from "../api";
55
import { cls, getUsername } from "../utils";
66

77
export const LoginButton = () => {
@@ -53,6 +53,7 @@ export const LoginButton = () => {
5353
href="#"
5454
onClick={() => {
5555
api["/api/dashboard/auth/logout"].post().then(() => {
56+
queryClient.clear();
5657
window.location.href = "/";
5758
});
5859
}}

0 commit comments

Comments
 (0)