diff --git a/backend/internal/token.js b/backend/internal/token.js
index 810ac6aa..f1d2b370 100644
--- a/backend/internal/token.js
+++ b/backend/internal/token.js
@@ -134,24 +134,24 @@ export default {
* @param {Object} user
* @returns {Promise}
*/
- getTokenFromUser: (user) => {
+ getTokenFromUser: async (user) => {
const expire = "1d";
const Token = new TokenModel();
const expiry = parseDatePeriod(expire);
- return Token.create({
+ const signed = await Token.create({
iss: "api",
attrs: {
id: user.id,
},
scope: ["user"],
expiresIn: expire,
- }).then((signed) => {
- return {
- token: signed.token,
- expires: expiry.toISOString(),
- user: user,
- };
});
+
+ return {
+ token: signed.token,
+ expires: expiry.toISOString(),
+ user: user,
+ };
},
};
diff --git a/backend/internal/user.js b/backend/internal/user.js
index e3d7ca4c..1c1f3a86 100644
--- a/backend/internal/user.js
+++ b/backend/internal/user.js
@@ -337,11 +337,11 @@ const internalUser = {
* @param {Integer} [id_requested]
* @returns {[String]}
*/
- getUserOmisionsByAccess: (access, id_requested) => {
+ getUserOmisionsByAccess: (access, idRequested) => {
let response = []; // Admin response
- if (!access.token.hasScope("admin") && access.token.getUserId(0) !== id_requested) {
- response = ["roles", "is_deleted"]; // Restricted response
+ if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
+ response = ["is_deleted"]; // Restricted response
}
return response;
diff --git a/backend/models/token.js b/backend/models/token.js
index 5edad90d..fb40e2a0 100644
--- a/backend/models/token.js
+++ b/backend/models/token.js
@@ -123,16 +123,16 @@ export default () => {
},
/**
- * @param [default_value]
+ * @param [defaultValue]
* @returns {Integer}
*/
- getUserId: (default_value) => {
+ getUserId: (defaultValue) => {
const attrs = self.get("attrs");
if (attrs && typeof attrs.id !== "undefined" && attrs.id) {
return attrs.id;
}
- return default_value || 0;
+ return defaultValue || 0;
},
};
diff --git a/frontend/package.json b/frontend/package.json
index eac969ec..c94566ff 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -30,6 +30,7 @@
"react-dom": "^19.1.1",
"react-intl": "^7.1.11",
"react-router-dom": "^7.8.2",
+ "react-toastify": "^11.0.5",
"rooks": "^9.2.0"
},
"devDependencies": {
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 1e642918..59677e8c 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -5,3 +5,10 @@
.domain-name {
font-family: monospace;
}
+
+.mr-1 {
+ margin-right: 0.25rem;
+}
+.ml-1 {
+ margin-left: 0.25rem;
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index aa2fb873..f2312f3f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { RawIntlProvider } from "react-intl";
+import { ToastContainer } from "react-toastify";
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
import { intl } from "src/locale";
import Router from "src/Router.tsx";
@@ -16,6 +17,15 @@ function App() {
+
diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts
index 745ce3ce..1bfccb4b 100644
--- a/frontend/src/api/backend/index.ts
+++ b/frontend/src/api/backend/index.ts
@@ -4,6 +4,7 @@ export * from "./createDeadHost";
export * from "./createProxyHost";
export * from "./createRedirectionHost";
export * from "./createStream";
+export * from "./createUser";
export * from "./deleteAccessList";
export * from "./deleteCertificate";
export * from "./deleteDeadHost";
diff --git a/frontend/src/components/HasPermission.tsx b/frontend/src/components/HasPermission.tsx
index 9b45e7f6..c4779b9e 100644
--- a/frontend/src/components/HasPermission.tsx
+++ b/frontend/src/components/HasPermission.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import Alert from "react-bootstrap/Alert";
+import { Loading, LoadingPage } from "src/components";
import { useUser } from "src/hooks";
import { intl } from "src/locale";
@@ -8,11 +9,30 @@ interface Props {
type: "manage" | "view";
hideError?: boolean;
children?: ReactNode;
+ pageLoading?: boolean;
+ loadingNoLogo?: boolean;
}
-function HasPermission({ permission, type, children, hideError = false }: Props) {
- const { data } = useUser("me");
+function HasPermission({
+ permission,
+ type,
+ children,
+ hideError = false,
+ pageLoading = false,
+ loadingNoLogo = false,
+}: Props) {
+ const { data, isLoading } = useUser("me");
const perms = data?.permissions;
+ if (isLoading) {
+ if (hideError) {
+ return null;
+ }
+ if (pageLoading) {
+ return ;
+ }
+ return ;
+ }
+
let allowed = permission === "";
const acceptable = ["manage", type];
diff --git a/frontend/src/components/LoadingPage.module.css b/frontend/src/components/Loading.module.css
similarity index 100%
rename from frontend/src/components/LoadingPage.module.css
rename to frontend/src/components/Loading.module.css
diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx
new file mode 100644
index 00000000..35a054db
--- /dev/null
+++ b/frontend/src/components/Loading.tsx
@@ -0,0 +1,22 @@
+import { intl } from "src/locale";
+import styles from "./Loading.module.css";
+
+interface Props {
+ label?: string;
+ noLogo?: boolean;
+}
+export function Loading({ label, noLogo }: Props) {
+ return (
+
+ {noLogo ? null : (
+
+

+
+ )}
+
{label || intl.formatMessage({ id: "loading" })}
+
+
+ );
+}
diff --git a/frontend/src/components/LoadingPage.tsx b/frontend/src/components/LoadingPage.tsx
index 6be77ebb..7d3bec1e 100644
--- a/frontend/src/components/LoadingPage.tsx
+++ b/frontend/src/components/LoadingPage.tsx
@@ -1,6 +1,4 @@
-import { Page } from "src/components";
-import { intl } from "src/locale";
-import styles from "./LoadingPage.module.css";
+import { Loading, Page } from "src/components";
interface Props {
label?: string;
@@ -10,17 +8,7 @@ export function LoadingPage({ label, noLogo }: Props) {
return (
-
- {noLogo ? null : (
-
-

-
- )}
-
{label || intl.formatMessage({ id: "loading" })}
-
-
+
);
diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts
index 66e12a74..be87e068 100644
--- a/frontend/src/components/index.ts
+++ b/frontend/src/components/index.ts
@@ -2,6 +2,7 @@ export * from "./Button";
export * from "./ErrorNotFound";
export * from "./Flag";
export * from "./HasPermission";
+export * from "./Loading";
export * from "./LoadingPage";
export * from "./LocalePicker";
export * from "./NavLink";
diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts
index 180032c2..fb991438 100644
--- a/frontend/src/hooks/useUser.ts
+++ b/frontend/src/hooks/useUser.ts
@@ -1,7 +1,20 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { getUser, type User, updateUser } from "src/api/backend";
+import { createUser, getUser, type User, updateUser } from "src/api/backend";
const fetchUser = (id: number | string) => {
+ if (id === "new") {
+ return Promise.resolve({
+ id: 0,
+ createdOn: "",
+ modifiedOn: "",
+ isDisabled: false,
+ email: "",
+ name: "",
+ nickname: "",
+ roles: [],
+ avatar: "",
+ } as User);
+ }
return getUser(id, { expand: "permissions" });
};
@@ -17,8 +30,11 @@ const useUser = (id: string | number, options = {}) => {
const useSetUser = () => {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (values: User) => updateUser(values),
+ mutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)),
onMutate: (values: User) => {
+ if (!values.id) {
+ return;
+ }
const previousObject = queryClient.getQueryData(["user", values.id]);
queryClient.setQueryData(["user", values.id], (old: User) => ({
...old,
diff --git a/frontend/src/locale/lang/en.json b/frontend/src/locale/lang/en.json
index eae027b8..f59a6d4d 100644
--- a/frontend/src/locale/lang/en.json
+++ b/frontend/src/locale/lang/en.json
@@ -51,6 +51,9 @@
"notfound.action": "Take me home",
"notfound.text": "We are sorry but the page you are looking for was not found",
"notfound.title": "Oops… You just found an error page",
+ "notification.error": "Error",
+ "notification.success": "Success",
+ "notification.user-saved": "User has been saved",
"offline": "Offline",
"online": "Online",
"password": "Password",
@@ -82,6 +85,7 @@
"user.edit-profile": "Edit Profile",
"user.full-name": "Full Name",
"user.logout": "Logout",
+ "user.new": "New User",
"user.new-password": "New Password",
"user.nickname": "Nickname",
"user.switch-dark": "Switch to Dark mode",
diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json
index b3e881de..d37553e9 100644
--- a/frontend/src/locale/src/en.json
+++ b/frontend/src/locale/src/en.json
@@ -155,6 +155,15 @@
"notfound.title": {
"defaultMessage": "Oops… You just found an error page"
},
+ "notification.error": {
+ "defaultMessage": "Error"
+ },
+ "notification.user-saved": {
+ "defaultMessage": "User has been saved"
+ },
+ "notification.success": {
+ "defaultMessage": "Success"
+ },
"offline": {
"defaultMessage": "Offline"
},
@@ -248,6 +257,9 @@
"user.logout": {
"defaultMessage": "Logout"
},
+ "user.new": {
+ "defaultMessage": "New User"
+ },
"user.new-password": {
"defaultMessage": "New Password"
},
diff --git a/frontend/src/modals/UserModal.tsx b/frontend/src/modals/UserModal.tsx
index 0f25817a..293b2ae9 100644
--- a/frontend/src/modals/UserModal.tsx
+++ b/frontend/src/modals/UserModal.tsx
@@ -2,31 +2,35 @@ import { Field, Form, Formik } from "formik";
import { useState } from "react";
import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
-import { Button } from "src/components";
+import { Button, Loading } from "src/components";
import { useSetUser, useUser } from "src/hooks";
import { intl } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
+import { showSuccess } from "src/notifications";
interface Props {
- userId: number | "me";
+ userId: number | "me" | "new";
onClose: () => void;
}
export function UserModal({ userId, onClose }: Props) {
- const { data } = useUser(userId);
- const { data: currentUser } = useUser("me");
+ const { data, isLoading, error } = useUser(userId);
+ const { data: currentUser, isLoading: currentIsLoading } = useUser("me");
const { mutate: setUser } = useSetUser();
- const [error, setError] = useState(null);
+ const [errorMsg, setErrorMsg] = useState(null);
+
+ if (data && currentUser) {
+ console.log("DATA:", data);
+ console.log("CURRENT:", currentUser);
+ }
const onSubmit = async (values: any, { setSubmitting }: any) => {
- setError(null);
+ setErrorMsg(null);
const { ...payload } = {
- id: userId,
+ id: userId === "new" ? undefined : userId,
roles: [],
...values,
};
- console.log("values", values);
-
if (data?.id === currentUser?.id) {
// Prevent user from locking themselves out
delete payload.isDisabled;
@@ -39,175 +43,188 @@ export function UserModal({ userId, onClose }: Props) {
delete payload.isAdmin;
setUser(payload, {
- onError: (err: any) => setError(err.message),
- onSuccess: () => onClose(),
+ onError: (err: any) => setErrorMsg(err.message),
+ onSuccess: () => {
+ showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
+ onClose();
+ },
onSettled: () => setSubmitting(false),
});
};
return (
-
- {({ isSubmitting }) => (
-