Skip to content

Commit bd5cb3c

Browse files
committed
wip: Add user management features and update environment configurations
1 parent 21abb1d commit bd5cb3c

22 files changed

+395
-52
lines changed

client/eslint.config.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default [
2525
URL: 'readonly',
2626
console: 'readonly',
2727
fetch: 'readonly',
28+
HTMLFormElement: 'readonly',
29+
FormData: 'readonly',
30+
FormDataEntryValue: 'readonly',
2831
},
2932
parserOptions: {
3033
ecmaFeatures: {
@@ -34,8 +37,8 @@ export default [
3437
},
3538
settings: {
3639
react: {
37-
version: 'detect'
38-
}
40+
version: '19'
41+
},
3942
},
4043
plugins: {
4144
react: reactPlugin,

client/package-lock.json

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

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"framer-motion": "^12.5.0",
3535
"i18next": "^24.2.3",
3636
"i18next-http-backend": "^3.0.2",
37+
"jose": "^6.0.10",
3738
"react": "^19.0.0",
3839
"react-dom": "^19.0.0",
3940
"react-i18next": "^15.4.1",

client/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { SiteLoading } from "./components/site-loading";
77
import DefaultLayout from "./layouts/default";
88
import { title } from "./components/primitives";
99
import { AuthenticationGuard, LogoutButton } from "./components/auth0";
10+
import { siteConfig } from "./config/site";
11+
import AddNewUser from "./pages/add-new-user";
1012

1113
import IndexPage from "@/pages/index";
1214
import ApiPage from "@/pages/api";
@@ -60,9 +62,16 @@ function App() {
6062
path="/blog"
6163
/>
6264
<Route element={<AboutPage />} path="/about" />
65+
{siteConfig().apiMenuItems.map((item) => (
66+
<Route
67+
key={item.href}
68+
element={<AuthenticationGuard component={AddNewUser} />}
69+
path={item.href}
70+
/>
71+
))}
6372
</Routes>
6473
</Suspense>
6574
);
6675
}
6776

68-
export default App;
77+
export default App;

client/src/components/auth0.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Tooltip } from "@heroui/tooltip";
2626
import { FC } from "react";
2727
import { useTranslation } from "react-i18next";
2828
import { Link } from "@heroui/link";
29+
import { createRemoteJWKSet, JWTPayload, jwtVerify } from "jose";
2930

3031
import { SiteLoading } from "./site-loading";
3132

@@ -292,7 +293,10 @@ export const LoginLogoutLink: FC<LogoutLinkProps> = ({
292293
* <AuthenticationGuard component={ProtectedDashboard} />
293294
* ```
294295
*/
295-
export const AuthenticationGuard: FC<{ component: FC }> = ({ component }) => {
296+
export const AuthenticationGuard: FC<{
297+
component: FC;
298+
permission?: string;
299+
}> = ({ component }) => {
296300
const Component = withAuthenticationRequired(component, {
297301
onRedirecting: () => (
298302
<>
@@ -349,3 +353,47 @@ export const getJsonFromSecuredApi = async (
349353
throw error;
350354
}
351355
};
356+
357+
/**
358+
* Checks if the user has a specific permission.
359+
* @param permission - The permission to check for
360+
* @param {GetAccessTokenFunction} getAccessTokenFunction - Function to retrieve an access token, typically Auth0's getAccessTokenSilently
361+
* @returns {Promise<boolean>} Promise resolving to true if the user has the permission, false otherwise
362+
* @throws {Error} If token acquisition fails or the API request fails
363+
*/
364+
export const userHasPermission = async (
365+
permission: string,
366+
getAccessTokenFunction: GetAccessTokenFunction,
367+
) => {
368+
try {
369+
const accessToken = await getAccessTokenFunction({
370+
authorizationParams: {
371+
audience: import.meta.env.AUTH0_AUDIENCE,
372+
scope: import.meta.env.AUTH0_SCOPE,
373+
},
374+
});
375+
376+
if (!accessToken) {
377+
return false;
378+
}
379+
const JWKS = createRemoteJWKSet(
380+
new URL(`https://${import.meta.env.AUTH0_DOMAIN}/.well-known/jwks.json`),
381+
);
382+
383+
const joseResult = await jwtVerify(accessToken, JWKS, {
384+
issuer: `https://${import.meta.env.AUTH0_DOMAIN}/`,
385+
audience: import.meta.env.AUTH0_AUDIENCE,
386+
});
387+
const payload = joseResult.payload as JWTPayload;
388+
389+
if (payload.permissions instanceof Array) {
390+
return payload.permissions.includes(permission);
391+
} else {
392+
return false;
393+
}
394+
} catch (error) {
395+
// eslint-disable-next-line no-console
396+
console.error(error);
397+
throw error;
398+
}
399+
};

client/src/components/icons.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,25 @@ export const SearchIcon = (props: IconSvgProps) => (
226226
/>
227227
</svg>
228228
);
229+
230+
export const ChevronDown = (props: IconSvgProps) => {
231+
return (
232+
<svg
233+
fill="none"
234+
height={props.size || props.height || 24}
235+
viewBox="0 0 24 24"
236+
width={props.size || props.width || 24}
237+
xmlns="http://www.w3.org/2000/svg"
238+
{...props}
239+
>
240+
<path
241+
d="m19.92 8.95-6.52 6.52c-.77.77-2.03.77-2.8 0L4.08 8.95"
242+
stroke={props.color || "currentColor"}
243+
strokeLinecap="round"
244+
strokeLinejoin="round"
245+
strokeMiterlimit={10}
246+
strokeWidth={1.5}
247+
/>
248+
</svg>
249+
);
250+
};

client/src/components/navbar.tsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ import {
1111
} from "@heroui/navbar";
1212
import { link as linkStyles } from "@heroui/theme";
1313
import { clsx } from "@heroui/shared-utils";
14-
import { Trans, useTranslation } from "react-i18next";
14+
import { useTranslation } from "react-i18next";
15+
import {
16+
Dropdown,
17+
DropdownItem,
18+
DropdownMenu,
19+
DropdownTrigger,
20+
} from "@heroui/dropdown";
1521

1622
import { I18nIcon, LanguageSwitch } from "./language-switch";
1723
import { LoginLogoutButton, LoginLogoutLink } from "./auth0";
1824

1925
import { siteConfig } from "@/config/site";
2026
import { ThemeSwitch } from "@/components/theme-switch";
21-
import { GithubIcon, HeartFilledIcon } from "@/components/icons";
27+
import { ChevronDown, GithubIcon } from "@/components/icons";
2228
import { Logo } from "@/components/icons";
2329
import { availableLanguages } from "@/i18n";
2430

@@ -54,6 +60,30 @@ export const Navbar = () => {
5460
</NavbarItem>
5561
))}
5662
</div>
63+
<NavbarItem>
64+
<Dropdown>
65+
<DropdownTrigger>
66+
<Button
67+
className={clsx(
68+
linkStyles({ color: "foreground" }),
69+
"data-[active=true]:text-primary data-[active=true]:font-medium",
70+
)}
71+
variant="light"
72+
>
73+
{t("Administration")} <ChevronDown />
74+
</Button>
75+
</DropdownTrigger>
76+
<DropdownMenu>
77+
{siteConfig().apiMenuItems.map((item) => (
78+
<DropdownItem key={item.href} textValue={item.label}>
79+
<Link color="foreground" href="/add-user">
80+
{item.label}
81+
</Link>
82+
</DropdownItem>
83+
))}
84+
</DropdownMenu>
85+
</Dropdown>
86+
</NavbarItem>
5787
</NavbarContent>
5888

5989
<NavbarContent
@@ -71,18 +101,6 @@ export const Navbar = () => {
71101
/>
72102
<LoginLogoutButton />
73103
</NavbarItem>
74-
<NavbarItem className="hidden md:flex">
75-
<Button
76-
isExternal
77-
as={Link}
78-
className="text-sm font-normal text-default-600 bg-default-100"
79-
href={siteConfig().links.sponsor}
80-
startContent={<HeartFilledIcon className="text-danger" />}
81-
variant="flat"
82-
>
83-
<Trans i18nKey="sponsor" />
84-
</Button>
85-
</NavbarItem>
86104
</NavbarContent>
87105

88106
<NavbarContent className="sm:hidden basis-1 pl-4" justify="end">

client/src/config/site.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ export const siteConfig = () => {
5353
href: "/about",
5454
},
5555
],
56+
apiMenuItems: [
57+
{
58+
label: i18next.t("add-a-new-user"),
59+
href: "/add-user",
60+
},
61+
],
5662
links: {
5763
github: "https://github.com/sctg-development/feedback-flow",
5864
sctg: "https://sctg.eu.org",

client/src/locales/base/ar-SA.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,11 @@
3737
"api-answer": "API API المضمون",
3838
"log-out-someone": "تسجيل الخروج {{name}}",
3939
"brand": "SCTG",
40-
"Feedback Flow": "Feedback Flow"
40+
"Feedback Flow": "Feedback Flow",
41+
"Administration": "إدارة",
42+
"add-a-new-user": "أضف مستخدمًا جديدًا",
43+
"enter-the-user-name": "أدخل اسم المستخدم",
44+
"please-enter-a-name": "الرجاء إدخال اسم",
45+
"submit": "يُقدِّم",
46+
"name": "اسم"
4147
}

client/src/locales/base/en-US.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,11 @@
3737
"api-answer": "Secured API answer",
3838
"log-out-someone": "Log out {{name}}",
3939
"brand": "SCTG",
40-
"Feedback Flow": "Feedback Flow"
40+
"Feedback Flow": "Feedback Flow",
41+
"Administration": "Administration",
42+
"add-a-new-user": "Add a new user",
43+
"please-enter-a-name": "Please enter a name",
44+
"enter-the-user-name": "Enter the user name",
45+
"submit": "Submit",
46+
"name": "Name"
4147
}

0 commit comments

Comments
 (0)