Skip to content

Commit f8a56c5

Browse files
feat: users can be granted access to projects
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 73d50ad commit f8a56c5

File tree

9 files changed

+111
-111
lines changed

9 files changed

+111
-111
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Since this is not a library, this changelog focuses on the changes that are rele
2222

2323
- Improved query caching to prevent unnecessary database queries
2424
- Added Country Code to Google Referrer URLs
25+
- Improved Multi-User Support (Non-admin users can now be granted access to specific projects)
2526

2627
## v1.0.0 - 2024-12-06
2728

Cargo.lock

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

src/app/core/sessions.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ impl LiwanSessions {
4242
Ok(models::User {
4343
username: row.get("username")?,
4444
role: row.get::<_, String>("role")?.try_into().unwrap_or_default(),
45-
projects: row.get::<_, String>("projects")?.split(',').map(str::to_string).collect(),
45+
projects: row
46+
.get::<_, String>("projects")?
47+
.split(',')
48+
.filter(|s| !s.is_empty())
49+
.map(str::to_string)
50+
.collect(),
4651
})
4752
});
4853

src/app/core/users.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ impl LiwanUsers {
3131
Ok(models::User {
3232
username: row.get("username")?,
3333
role: row.get::<_, String>("role")?.try_into().unwrap_or_default(),
34-
projects: row.get::<_, String>("projects")?.split(',').map(str::to_string).collect(),
34+
projects: row
35+
.get::<_, String>("projects")?
36+
.split(',')
37+
.filter(|s| !s.is_empty())
38+
.map(str::to_string)
39+
.collect(),
3540
})
3641
});
3742
user.map_err(|_| eyre::eyre!("user not found"))
@@ -45,7 +50,12 @@ impl LiwanUsers {
4550
Ok(models::User {
4651
username: row.get("username")?,
4752
role: row.get::<_, String>("role")?.try_into().unwrap_or_default(),
48-
projects: row.get::<_, String>("projects")?.split(',').map(str::to_string).collect(),
53+
projects: row
54+
.get::<_, String>("projects")?
55+
.split(',')
56+
.filter(|s| !s.is_empty())
57+
.map(str::to_string)
58+
.collect(),
4959
})
5060
})?;
5161
Ok(users.collect::<Result<Vec<models::User>, rusqlite::Error>>()?)

tracker/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"homepage": "https://liwan.dev",
55
"repository": {
66
"type": "git",
7-
"url": "https://github.com/explodingcamera/liwan",
7+
"url": "git+https://github.com/explodingcamera/liwan.git",
88
"directory": "tracker"
99
},
1010
"license": "MIT",

web/bun.lockb

-440 Bytes
Binary file not shown.

web/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
"@icons-pack/react-simple-icons": "^10.2.0",
1616
"@nivo/line": "^0.88.0",
1717
"@picocss/pico": "^2.0.6",
18-
"@radix-ui/react-accordion": "^1.2.1",
19-
"@radix-ui/react-dialog": "^1.1.2",
20-
"@radix-ui/react-tabs": "^1.1.1",
18+
"@radix-ui/react-accordion": "^1.2.2",
19+
"@radix-ui/react-dialog": "^1.1.3",
20+
"@radix-ui/react-tabs": "^1.1.2",
2121
"@tanstack/react-query": "^5.62.7",
2222
"d3-array": "^3.2.4",
2323
"d3-axis": "^3.0.0",

web/src/components/settings/dialogs.tsx

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,11 @@ const roles = ["admin", "user"] as const;
523523

524524
export const EditUser = ({ user, trigger }: { user: UserResponse; trigger: ReactElement }) => {
525525
const closeRef = useRef<HTMLButtonElement>(null);
526+
527+
const { projects } = useProjects();
528+
const projectTags = useMemo(() => projects.map((p) => ({ value: p.id, label: p.displayName })), [projects]);
529+
const [selectedProjects, setSelectedProjects] = useState<Tag[]>([]);
530+
526531
const { mutate, error, reset } = useMutation({
527532
mutationFn: api["/api/dashboard/user/{username}"].put,
528533
onSuccess: () => {
@@ -533,12 +538,31 @@ export const EditUser = ({ user, trigger }: { user: UserResponse; trigger: React
533538
onError: console.error,
534539
});
535540

541+
// biome-ignore lint/correctness/useExhaustiveDependencies: don't want to re-run this effect when projects change
542+
useEffect(() => {
543+
setSelectedProjects(
544+
user.projects.map((projectId) => {
545+
const p = projects.find((p) => p.id === projectId);
546+
return {
547+
value: projectId,
548+
label: p ? p.displayName : projectId,
549+
};
550+
}),
551+
);
552+
}, [user.projects]);
553+
536554
const handleSubmit = (e: React.FormEvent) => {
537555
e.preventDefault();
538556
e.stopPropagation();
557+
539558
const form = e.target as HTMLFormElement;
540-
const { role } = Object.fromEntries(new FormData(form)) as { role: (typeof roles)[number] };
541-
mutate({ params: { username: user.username }, json: { role, projects: [] } });
559+
const { admin } = Object.fromEntries(new FormData(form)) as { admin: string };
560+
const role = admin === "on" ? "admin" : "user";
561+
562+
mutate({
563+
params: { username: user.username },
564+
json: { role, projects: selectedProjects.map((tag) => tag.value as string) },
565+
});
542566
};
543567

544568
return (
@@ -550,16 +574,23 @@ export const EditUser = ({ user, trigger }: { user: UserResponse; trigger: React
550574
trigger={trigger}
551575
>
552576
<form onSubmit={handleSubmit}>
577+
<Tags
578+
labelText="Projects"
579+
selected={selectedProjects}
580+
suggestions={projectTags}
581+
onAdd={(tag) => setSelectedProjects((rest) => [...rest, tag])}
582+
onDelete={(i) => setSelectedProjects(selectedProjects.filter((_, index) => index !== i))}
583+
noOptionsText="No matching projects"
584+
/>
553585
<label>
554-
Role
555-
<select name="role" defaultValue={user.role}>
556-
{roles.map((r) => (
557-
<option key={r} value={r}>
558-
{r}
559-
</option>
560-
))}
561-
</select>
586+
{/* biome-ignore lint/a11y/useAriaPropsForRole: this is an uncontrolled component */}
587+
<input name="admin" type="checkbox" role="switch" defaultChecked={user.role === "admin"} />
588+
Enable Administrator Access
589+
<br />
590+
<small>Administators can edit and create projects, entities, and users.</small>
562591
</label>
592+
<br />
593+
563594
<div className="grid">
564595
<Dialog.Close asChild>
565596
<button className="secondary outline" type="button" ref={closeRef}>

0 commit comments

Comments
 (0)