Skip to content

Commit 8f6a465

Browse files
committed
wip(feat): add support for user scopes
Signed-off-by: SoulHarsh007 <[email protected]>
1 parent 717fed0 commit 8f6a465

File tree

20 files changed

+430
-220
lines changed

20 files changed

+430
-220
lines changed

CachyOS Builder Dashboard.code-workspace

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"sonner",
2222
"tabler",
2323
"toastify",
24+
"unrs",
2425
"Upsert",
2526
"vercel",
2627
"webgl",

bun.lock

Lines changed: 87 additions & 91 deletions
Large diffs are not rendered by default.

eslint.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ const eslintConfig = [
1717
'next-env.d.ts',
1818
],
1919
},
20+
{
21+
// @todo: Enable this rule once the existing errors are fixed.
22+
rules: {
23+
'react-hooks/set-state-in-effect': 'off',
24+
},
25+
},
2026
];
2127

2228
export default eslintConfig;

package.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"dependencies": {
33
"@hookform/resolvers": "^5.2.2",
4-
"@marsidev/react-turnstile": "1.3.1",
4+
"@marsidev/react-turnstile": "1.4.0",
55
"@radix-ui/react-avatar": "^1.1.11",
66
"@radix-ui/react-checkbox": "^1.3.3",
77
"@radix-ui/react-collapsible": "^1.1.12",
@@ -15,7 +15,7 @@
1515
"@radix-ui/react-slot": "^1.2.4",
1616
"@radix-ui/react-switch": "^1.2.6",
1717
"@radix-ui/react-tooltip": "^1.2.8",
18-
"@tabler/icons-react": "3.35.0",
18+
"@tabler/icons-react": "3.36.0",
1919
"@tanstack/react-table": "^8.21.3",
2020
"@xterm/addon-fit": "0.10.0",
2121
"@xterm/addon-search": "0.15.0",
@@ -26,39 +26,39 @@
2626
"clsx": "^2.1.1",
2727
"cmdk": "^1.1.1",
2828
"iron-session": "8.0.4",
29-
"lucide-react": "^0.554.0",
30-
"next": "16.0.10",
29+
"lucide-react": "^0.562.0",
30+
"next": "16.1.0",
3131
"next-themes": "^0.4.6",
3232
"pretty-bytes": "7.1.0",
3333
"pretty-ms": "9.3.0",
3434
"react": "19.2.3",
3535
"react-dom": "19.2.3",
36-
"react-hook-form": "7.66.1",
37-
"recharts": "^3.5.0",
36+
"react-hook-form": "7.69.0",
37+
"recharts": "^3.6.0",
3838
"sonner": "^2.0.7",
3939
"strip-ansi": "7.1.2",
4040
"tailwind-merge": "^3.4.0",
4141
"tailwind-variants": "3.2.2",
4242
"use-debounce": "10.0.6",
43-
"zod": "^4.1.13"
43+
"zod": "^4.2.1"
4444
},
4545
"devDependencies": {
46-
"@tailwindcss/postcss": "^4.1.17",
47-
"@types/node": "^24.10.1",
46+
"@tailwindcss/postcss": "^4.1.18",
47+
"@types/node": "^25.0.3",
4848
"@types/react": "19.2.7",
4949
"@types/react-dom": "19.2.3",
50-
"eslint": "^9.39.1",
51-
"eslint-config-next": "16.0.10",
50+
"eslint": "^9.39.2",
51+
"eslint-config-next": "16.1.0",
5252
"eslint-config-prettier": "^10.1.8",
53-
"eslint-plugin-perfectionist": "^4.15.1",
53+
"eslint-plugin-perfectionist": "^5.0.0",
5454
"eslint-plugin-prettier": "^5.5.4",
55-
"prettier": "^3.6.2",
56-
"tailwindcss": "^4.1.17",
55+
"prettier": "^3.7.4",
56+
"tailwindcss": "^4.1.18",
5757
"tw-animate-css": "^1.4.0",
5858
"typescript": "^5.9.3"
5959
},
6060
"name": "cachyos-builder-dashboard",
61-
"packageManager": "[email protected].3",
61+
"packageManager": "[email protected].4",
6262
"private": true,
6363
"scripts": {
6464
"build": "next build",

src/app/actions/session.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export async function getAccessibleServers() {
3636
return redirect('/');
3737
}
3838
return session.tokens.map((token, index) => ({
39-
accessible: token.token !== '',
39+
accessible: token.token !== '' && token.scopes.length > 0,
4040
active: index === session.serverIndex,
4141
description: token.description,
4242
name: token.name,
@@ -66,11 +66,13 @@ export async function getLoggedInUser(fullProfile = false) {
6666
user.profile_picture_url ?? '/cachyos-logo.svg';
6767
await session.save();
6868
if (fullProfile) {
69+
user.scopes = session.tokens[session.serverIndex].scopes;
6970
return user;
7071
}
7172
return {
7273
displayName: session.displayName,
7374
profile_picture_url: session.profile_picture_url,
75+
scopes: session.tokens[session.serverIndex].scopes,
7476
username: session.username,
7577
};
7678
} catch (error) {
@@ -94,7 +96,7 @@ export async function getSession() {
9496
}
9597
const cachyBuilderClient = new CachyBuilderClient(
9698
session.serverIndex,
97-
session.tokens[session.serverIndex].token
99+
session.tokens
98100
);
99101
return {
100102
cachyBuilderClient,
@@ -167,3 +169,29 @@ export async function logout() {
167169
session.destroy();
168170
return redirect('/');
169171
}
172+
173+
export async function syncLoggedInUserScopes() {
174+
const {cachyBuilderClient, session} = await getSession();
175+
if (!session.isLoggedIn) {
176+
return redirect('/');
177+
}
178+
try {
179+
const {errors, tokens} = await cachyBuilderClient.syncLoggedInUserScopes(
180+
true,
181+
await headers()
182+
);
183+
session.tokens = tokens;
184+
await session.save();
185+
return {
186+
success: tokens.length > 0,
187+
warning:
188+
errors.length > 0
189+
? `Failed to sync scopes on some servers, these servers will be disabled for current session:\n${errors}`
190+
: undefined,
191+
};
192+
} catch (error) {
193+
return {
194+
error: `Failed to sync user scopes: ${error instanceof Error ? error.message : 'Unknown error'}`,
195+
};
196+
}
197+
}

src/app/actions/users.ts

Lines changed: 1 addition & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,7 @@ import {headers} from 'next/headers';
44
import {redirect} from 'next/navigation';
55

66
import {getSession} from '@/app/actions/session';
7-
import {UserData, UserProfile} from '@/lib/typings';
8-
9-
export async function changeServer(serverName: string) {
10-
const {session} = await getSession();
11-
const serverIndex = session.tokens.findIndex(
12-
token => token.name === serverName && token.token !== ''
13-
);
14-
if (serverIndex === -1) {
15-
return {
16-
error: `Server "${serverName}" not found or is not accessible with the current session.`,
17-
};
18-
}
19-
session.serverIndex = serverIndex;
20-
await session.save();
21-
return {
22-
msg: `Switched to server "${serverName}" successfully.`,
23-
};
24-
}
25-
26-
export async function getAccessibleServers() {
27-
const {session} = await getSession();
28-
if (!session.isLoggedIn) {
29-
return redirect('/');
30-
}
31-
return session.tokens.map((token, index) => ({
32-
accessible: token.token !== '',
33-
active: index === session.serverIndex,
34-
description: token.description,
35-
name: token.name,
36-
}));
37-
}
38-
39-
export async function getLoggedInUser(
40-
fullProfile: false
41-
): Promise<UserData | {error: string}>;
42-
43-
export async function getLoggedInUser(
44-
fullProfile: true
45-
): Promise<UserProfile | {error: string}>;
46-
47-
export async function getLoggedInUser(fullProfile = false) {
48-
const {cachyBuilderClient, session} = await getSession();
49-
if (!session.isLoggedIn) {
50-
return redirect('/');
51-
}
52-
try {
53-
const user = await cachyBuilderClient.getLoggedInUserProfile(
54-
await headers()
55-
);
56-
session.displayName = user.display_name ?? user.username;
57-
session.username = user.username;
58-
session.profile_picture_url =
59-
user.profile_picture_url ?? '/cachyos-logo.svg';
60-
await session.save();
61-
if (fullProfile) {
62-
return user;
63-
}
64-
return {
65-
displayName: session.displayName,
66-
profile_picture_url: session.profile_picture_url,
67-
username: session.username,
68-
};
69-
} catch (error) {
70-
return {
71-
error: `Failed to get user profile: ${error instanceof Error ? error.message : 'Unknown error'}`,
72-
};
73-
}
74-
}
7+
import {UserProfile} from '@/lib/typings';
758

769
export async function getUser(username: string) {
7710
const {cachyBuilderClient, session} = await getSession();

src/app/dashboard/package-list/page.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ import {
4949
packageRepoValues,
5050
PackageStatus,
5151
packageStatusValues,
52+
UserScope,
5253
} from '@/lib/typings';
53-
import {packageStatusToIcon} from '@/lib/utils';
54+
import {checkScopes, packageStatusToIcon} from '@/lib/utils';
5455

5556
export default function PackageListPage() {
56-
const {activeServer} = useSidebar();
57+
const {activeServer, scopes} = useSidebar();
5758
const [data, setData] = useState<ListPackageResponse | null>(null);
5859
const [error, setError] = useState<null | string>(null);
5960
const [pageSize, setPageSize] = useState(20);
@@ -85,6 +86,10 @@ export default function PackageListPage() {
8586

8687
useGenericShortcutListener('/', primarySearchFilterShortcutCallback, true);
8788

89+
const enableRebuild = useMemo(
90+
() => checkScopes(scopes, [UserScope.READ, UserScope.WRITE]),
91+
[scopes]
92+
);
8893
const columns: ColumnDef<Package>[] = useMemo(
8994
() => [
9095
{
@@ -241,7 +246,12 @@ export default function PackageListPage() {
241246
</DropdownMenuTrigger>
242247
<DropdownMenuContent align="end" className="max-w-48">
243248
<DropdownMenuItem
249+
disabled={!enableRebuild}
250+
hidden={!enableRebuild}
244251
onSelect={() => {
252+
if (!enableRebuild) {
253+
return;
254+
}
245255
const toastId = toast.loading(
246256
`Requesting rebuild for PkgBase: ${row.original.pkgbase} MArch: ${row.original.march} Repo: ${row.original.repository}...`
247257
);
@@ -271,7 +281,7 @@ export default function PackageListPage() {
271281
>
272282
<RotateCcw /> Rebuild
273283
</DropdownMenuItem>
274-
<DropdownMenuSeparator />
284+
<DropdownMenuSeparator hidden={!enableRebuild} />
275285
<DropdownMenuItem
276286
asChild
277287
disabled={row.original.status !== PackageStatus.FAILED}
@@ -302,7 +312,7 @@ export default function PackageListPage() {
302312
id: 'actions',
303313
},
304314
],
305-
[]
315+
[enableRebuild]
306316
);
307317

308318
const onMarchFilterUpdate = useCallback(
@@ -457,6 +467,7 @@ export default function PackageListPage() {
457467
<div className="flex">
458468
<Button
459469
className="h-8"
470+
disabled={!enableRebuild}
460471
onClick={() => setShowRebuildModal(true)}
461472
size="sm"
462473
variant="outline"

src/app/dashboard/profile/page.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
'use client';
2-
import {useCallback, useEffect, useState} from 'react';
2+
import {useCallback, useEffect, useMemo, useState} from 'react';
33
import {toast} from 'sonner';
44

5-
import {getLoggedInUser} from '@/app/actions/users';
5+
import {getLoggedInUser} from '@/app/actions/session';
66
import Loader from '@/components/loader';
77
import {Card} from '@/components/ui/card';
88
import {useSidebar} from '@/components/ui/sidebar';
99
import {UserProfileForm} from '@/components/user-profile-form';
10-
import {UserProfile} from '@/lib/typings';
10+
import {UserProfile, UserScope} from '@/lib/typings';
11+
import {checkScopes} from '@/lib/utils';
1112

1213
export default function UserProfilePage() {
13-
const {activeServer, doRefresh} = useSidebar();
14+
const {activeServer, doRefresh, scopes} = useSidebar();
1415
const [user, setUser] = useState<null | UserProfile>(null);
1516
const onUserUpdate = useCallback(
1617
(updatedUser: UserProfile) => {
@@ -19,6 +20,10 @@ export default function UserProfilePage() {
1920
},
2021
[doRefresh]
2122
);
23+
const enableEdits = useMemo(
24+
() => checkScopes(scopes, [UserScope.READ, UserScope.WRITE]),
25+
[scopes]
26+
);
2227
useEffect(() => {
2328
setUser(null);
2429
getLoggedInUser(true).then(data => {
@@ -36,7 +41,11 @@ export default function UserProfilePage() {
3641
<Card className="flex min-h-full w-full items-center justify-center p-6 md:p-10">
3742
<div className="w-full max-w-lg">
3843
{user ? (
39-
<UserProfileForm onUserUpdate={onUserUpdate} user={user} />
44+
enableEdits ? (
45+
<UserProfileForm onUserUpdate={onUserUpdate} user={user} />
46+
) : (
47+
<UserProfileForm disabled onUserUpdate={() => {}} user={user} />
48+
)
4049
) : (
4150
<Loader animate text="Loading user profile..." />
4251
)}

0 commit comments

Comments
 (0)