diff --git a/frontend/src/components/admin/user/UserPage.tsx b/frontend/src/components/admin/user/UserPage.tsx
new file mode 100644
index 0000000..01df3ea
--- /dev/null
+++ b/frontend/src/components/admin/user/UserPage.tsx
@@ -0,0 +1,43 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { getUsers } from "@/utils/querys/admin/users";
+import { useQuery } from "@tanstack/react-query";
+import UserCard from "./components/UserCard";
+
+export default function UserPage() {
+ const { data, isError, error, isLoading } = useQuery(getUsers());
+
+ if (isLoading) return
Loading users...
;
+
+ if (isError)
+ return (
+
+ There was an error getting the users.... {JSON.stringify(error)}
+
+ );
+
+ return (
+
+
+ {data.map((u: any) => {
+ return (
+
+
+ {u.username}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/src/components/admin/user/components/UserCard.tsx b/frontend/src/components/admin/user/components/UserCard.tsx
new file mode 100644
index 0000000..0effadd
--- /dev/null
+++ b/frontend/src/components/admin/user/components/UserCard.tsx
@@ -0,0 +1,183 @@
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { userFormOptions } from "@/utils/formStuff/options/userformOptions";
+import { generatePassword } from "@/utils/passwordGen";
+import { useForm } from "@tanstack/react-form";
+import axios from "axios";
+import { toast } from "sonner";
+
+export default function UserCard(data: any) {
+ const token = localStorage.getItem("auth_token");
+ const form = useForm({
+ ...userFormOptions(data.user),
+ onSubmit: async ({ value }) => {
+ // Do something with form data
+
+ const userData = { ...value, user_id: data.user.user_id };
+
+ try {
+ const res = await axios.patch(
+ "/api/auth/updateuser",
+ userData,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ if (res.data.success) {
+ toast.success(res.data.message);
+ form.reset();
+ } else {
+ res.data.message;
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ },
+ });
+ return (
+
+
+ value.length > 3
+ ? undefined
+ : "Username must be longer than 3 letters",
+ }}
+ children={(field) => {
+ return (
+
+ Username
+
+ field.handleChange(e.target.value)
+ }
+ />
+ {field.state.meta.errors.length ? (
+ {field.state.meta.errors.join(",")}
+ ) : null}
+
+ );
+ }}
+ />
+
+ value.length > 3
+ ? undefined
+ : "You must enter a correct ",
+ }}
+ children={(field) => {
+ return (
+
+ Email
+
+ field.handleChange(e.target.value)
+ }
+ />
+ {field.state.meta.errors.length ? (
+ {field.state.meta.errors.join(",")}
+ ) : null}
+
+ );
+ }}
+ />
+ {
+ if (
+ window.location.pathname.includes("/users") &&
+ value.length === 0
+ ) {
+ return;
+ }
+ if (value.length < 4) {
+ return "Password must be at least 4 characters long.";
+ }
+
+ if (!/[A-Z]/.test(value)) {
+ return "Password must contain at least one uppercase letter.";
+ }
+
+ if (!/[a-z]/.test(value)) {
+ return "Password must contain at least one lower case letter.";
+ }
+
+ if (!/[0-9]/.test(value)) {
+ return "Password must contain at least one number.";
+ }
+
+ if (
+ !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(
+ value
+ )
+ ) {
+ return "Password must contain at least one special character.";
+ }
+ },
+ }}
+ children={(field) => {
+ return (
+
+
+ Change Password
+
+
+
+ field.handleChange(e.target.value)
+ }
+ />
+
+ field.handleChange(
+ generatePassword(8)
+ )
+ }
+ >
+ Random password
+
+
+ {field.state.meta.errors.length ? (
+
{field.state.meta.errors.join(",")}
+ ) : null}
+
+ );
+ }}
+ />
+
+
+ Save
+
+
+ );
+}
diff --git a/frontend/src/components/layout/side-components/admin.tsx b/frontend/src/components/layout/side-components/admin.tsx
index d8a8fba..0d782d4 100644
--- a/frontend/src/components/layout/side-components/admin.tsx
+++ b/frontend/src/components/layout/side-components/admin.tsx
@@ -1,4 +1,14 @@
-import {Atom, Logs, Minus, Plus, Server, Settings, ShieldCheck, Users, Webhook} from "lucide-react";
+import {
+ Atom,
+ Logs,
+ Minus,
+ Plus,
+ Server,
+ Settings,
+ ShieldCheck,
+ Users,
+ Webhook,
+} from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
@@ -10,7 +20,11 @@ import {
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "../../ui/sidebar";
-import {Collapsible, CollapsibleContent, CollapsibleTrigger} from "../../ui/collapsible";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "../../ui/collapsible";
const items = [
{
@@ -53,9 +67,9 @@ const data = {
},
{
title: "Users",
- url: "#",
+ url: "/users",
icon: Users,
- isActive: false,
+ isActive: true,
},
{
title: "UCD",
@@ -82,7 +96,11 @@ export function AdminSideBar() {
{data.navMain.map((item, index) => (
-
+
@@ -96,15 +114,25 @@ export function AdminSideBar() {
{item.items.map((item) => (
-
+
{item.isActive && (
-
+
- {item.title}
+
+ {item.title}
+
)}
diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..d21b65f
--- /dev/null
+++ b/frontend/src/components/ui/accordion.tsx
@@ -0,0 +1,64 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDownIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/frontend/src/routes/_admin/users.tsx b/frontend/src/routes/_admin/users.tsx
new file mode 100644
index 0000000..93b033f
--- /dev/null
+++ b/frontend/src/routes/_admin/users.tsx
@@ -0,0 +1,10 @@
+import UserPage from "@/components/admin/user/UserPage";
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/_admin/users")({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ return ;
+}
diff --git a/frontend/src/utils/formStuff/options/PasswordField.tsx b/frontend/src/utils/formStuff/options/PasswordField.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/utils/formStuff/options/userformOptions.tsx b/frontend/src/utils/formStuff/options/userformOptions.tsx
new file mode 100644
index 0000000..6e32a7a
--- /dev/null
+++ b/frontend/src/utils/formStuff/options/userformOptions.tsx
@@ -0,0 +1,13 @@
+import { formOptions } from "@tanstack/react-form";
+
+export const userFormOptions = (user: any) => {
+ return formOptions({
+ defaultValues: {
+ username: user.username,
+ password: "",
+ email: user.email,
+ //hobbies: [],
+ },
+ // } as Person,
+ });
+};
diff --git a/frontend/src/utils/passwordGen.ts b/frontend/src/utils/passwordGen.ts
new file mode 100644
index 0000000..4841735
--- /dev/null
+++ b/frontend/src/utils/passwordGen.ts
@@ -0,0 +1,27 @@
+export const generatePassword = (length: number) => {
+ const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ const lowercase = "abcdefghijklmnopqrstuvwxyz";
+ const numbers = "0123456789";
+ const symbols = "!@#$%&()_+-={}:,.<>?/"; // Safe symbol list
+
+ // Ensure the password contains at least one of each required type
+ let password: any = [
+ uppercase[Math.floor(Math.random() * uppercase.length)],
+ lowercase[Math.floor(Math.random() * lowercase.length)],
+ numbers[Math.floor(Math.random() * numbers.length)],
+ symbols[Math.floor(Math.random() * symbols.length)],
+ ];
+
+ // Fill the rest of the password with random characters from all sets
+ const allCharacters = uppercase + lowercase;
+ for (let i = password.length; i < length; i++) {
+ password.push(
+ allCharacters[Math.floor(Math.random() * allCharacters.length)]
+ );
+ }
+
+ // Shuffle the password to avoid predictable patterns
+ password = password.sort(() => Math.random() - 0.5).join("");
+
+ return password;
+};
diff --git a/frontend/src/utils/querys/admin/users.tsx b/frontend/src/utils/querys/admin/users.tsx
new file mode 100644
index 0000000..29f893d
--- /dev/null
+++ b/frontend/src/utils/querys/admin/users.tsx
@@ -0,0 +1,26 @@
+import { queryOptions } from "@tanstack/react-query";
+import axios from "axios";
+
+export function getUsers() {
+ const token = localStorage.getItem("auth_token");
+ return queryOptions({
+ queryKey: ["getUsers"],
+ queryFn: () => fetchUsers(token),
+ enabled: !!token, // Prevents query if token is null
+ staleTime: 1000,
+ //refetchInterval: 2 * 2000,
+ refetchOnWindowFocus: true,
+ });
+}
+
+const fetchUsers = async (token: string | null) => {
+ const { data } = await axios.get(`/api/auth/allusers`, {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ // if we are not localhost ignore the devDir setting.
+ //const url: string = window.location.host.split(":")[0];
+ return data.data ?? [];
+};
diff --git a/server/services/auth/controllers/userAdmin/updateUserAdm.ts b/server/services/auth/controllers/userAdmin/updateUserAdm.ts
index 0d376ab..1d291da 100644
--- a/server/services/auth/controllers/userAdmin/updateUserAdm.ts
+++ b/server/services/auth/controllers/userAdmin/updateUserAdm.ts
@@ -5,64 +5,78 @@ import { tryCatch } from "../../../../globalUtils/tryCatch.js";
import type { User } from "../../../../types/users.js";
import { createPassword } from "../../utils/createPassword.js";
import { createLog } from "../../../logger/logger.js";
+import { sendEmail } from "../../../notifications/controller/sendMail.js";
export const updateUserADM = async (userData: User) => {
- /**
- * The user model will need to be passed over so we can update per the request on the user.
- * password, username, email.
- */
+ /**
+ * The user model will need to be passed over so we can update per the request on the user.
+ * password, username, email.
+ */
- createLog(
- "info",
- "apiAuthedRoute",
- "auth",
- `${userData.user_id} is being updated.`
- );
- // get the orignal user info
- const { data: user, error: userError } = await tryCatch(
- db.select().from(users).where(eq(users.user_id, userData.user_id!))
- );
+ createLog(
+ "info",
+ "apiAuthedRoute",
+ "auth",
+ `${userData.user_id} is being updated.`
+ );
+ // get the orignal user info
+ const { data: user, error: userError } = await tryCatch(
+ db.select().from(users).where(eq(users.user_id, userData.user_id!))
+ );
- if (userError) {
- return {
- success: false,
- message: "There was an error getting the user",
- userError,
+ if (userError) {
+ return {
+ success: false,
+ message: "There was an error getting the user",
+ userError,
+ };
+ }
+ if (user?.length === 0) {
+ return {
+ success: false,
+ message:
+ "The user you are looking for has either been deleted or dose not exist.",
+ };
+ }
+ const upd_user = user as User;
+ const password: string = userData.password
+ ? await createPassword(userData.password!)
+ : upd_user.password!;
+ const data = {
+ username: userData.username ? userData.username : upd_user?.username,
+ password: password,
+ email: userData.email ? userData.email : upd_user.email,
};
- }
- if (user?.length === 0) {
+
+ // term ? ilike(posts.title, term) : undefined
+ const { data: updData, error: updError } = await tryCatch(
+ db.update(users).set(data).where(eq(users.user_id, userData.user_id!))
+ );
+
+ if (updError) {
+ return {
+ success: false,
+ message: "There was an error getting the user",
+ updError,
+ };
+ }
+
+ if (userData?.password!.length > 0) {
+ // send this user an email so they have the randomized password.
+ await sendEmail({
+ email: user[0]?.email,
+ subject: "LST - Password reset.",
+ template: "passwordReset",
+ context: {
+ password: userData.password!,
+ username: user[0].username!,
+ },
+ });
+ }
+
return {
- success: false,
- message:
- "The user you are looking for has either been deleted or dose not exist.",
+ success: true,
+ message: `${userData.username} has been updated.`,
+ updData,
};
- }
- const upd_user = user as User;
- const password: string = userData.password
- ? await createPassword(userData.password!)
- : upd_user.password!;
- const data = {
- username: userData.username ? userData.username : upd_user?.username,
- password: password,
- email: userData.email ? userData.email : upd_user.email,
- };
-
- // term ? ilike(posts.title, term) : undefined
- const { data: updData, error: updError } = await tryCatch(
- db.update(users).set(data).where(eq(users.user_id, userData.user_id!))
- );
-
- if (updError) {
- return {
- success: false,
- message: "There was an error getting the user",
- updError,
- };
- }
-
- return {
- success: true,
- message: `${userData.username} has been updated.`,
- updData,
- };
};
diff --git a/server/services/auth/routes/userAdmin/updateUser.ts b/server/services/auth/routes/userAdmin/updateUser.ts
index 21f322b..e0a5488 100644
--- a/server/services/auth/routes/userAdmin/updateUser.ts
+++ b/server/services/auth/routes/userAdmin/updateUser.ts
@@ -10,82 +10,76 @@ import { updateUserADM } from "../../controllers/userAdmin/updateUserAdm.js";
const app = new OpenAPIHono();
const responseSchema = z.object({
- success: z.boolean().openapi({ example: true }),
- message: z.string().optional().openapi({ example: "user access" }),
- data: z.array(z.object({})).optional().openapi({ example: [] }),
+ success: z.boolean().openapi({ example: true }),
+ message: z.string().optional().openapi({ example: "user access" }),
+ data: z.array(z.object({})).optional().openapi({ example: [] }),
});
const UserAccess = z.object({
- user_id: z.string().openapi({ example: "users UUID" }),
- username: z
- .string()
- .regex(/^[a-zA-Z0-9_]{3,30}$/)
- .optional()
- .openapi({ example: "smith034" }),
- email: z
- .string()
- .email()
- .optional()
- .openapi({ example: "smith@example.com" }),
- password: z
- .string()
- .min(6, { message: "Passwords must be longer than 3 characters" })
- .regex(/[A-Z]/, {
- message: "Password must contain at least one uppercase letter",
- })
- .regex(/[\W_]/, {
- message: "Password must contain at least one special character",
- })
- .optional()
- .openapi({ example: "Password1!" }),
+ user_id: z.string().openapi({ example: "users UUID" }),
+ username: z
+ .string()
+ .regex(/^[a-zA-Z0-9_]{3,30}$/)
+ .optional()
+ .openapi({ example: "smith034" }),
+ email: z
+ .string()
+ .email()
+ .optional()
+ .openapi({ example: "smith@example.com" }),
+ password: z
+ .string()
+
+ .optional()
+ .openapi({ example: "Password1!" }),
});
app.openapi(
- createRoute({
- tags: ["Auth:admin"],
- summary: "updates a specific user",
- method: "post",
- path: "/updateuser",
- middleware: [
- authMiddleware,
- hasCorrectRole(["admin", "systemAdmin"], "admin"),
- ],
- //description: "When logged in you will be able to grant new permissions",
- request: {
- body: {
- content: {
- "application/json": { schema: UserAccess },
+ createRoute({
+ tags: ["Auth:admin"],
+ summary: "updates a specific user",
+ method: "patch",
+ path: "/updateuser",
+ middleware: [
+ authMiddleware,
+ hasCorrectRole(["admin", "systemAdmin"], "admin"),
+ ],
+ //description: "When logged in you will be able to grant new permissions",
+ request: {
+ body: {
+ content: {
+ "application/json": { schema: UserAccess },
+ },
+ },
},
- },
- },
- responses: responses(),
- }),
- async (c) => {
- //apiHit(c, { endpoint: "api/auth/setUserRoles" });
- const userData = await c.req.json();
- try {
- const userUPD: any = await updateUserADM(userData);
- //return apiReturn(c, true, access?.message, access?.data, 200);
- return c.json(
- {
- success: userUPD.success,
- message: userUPD.message,
- data: userUPD.data,
- },
- 200
- );
- } catch (error) {
- console.log(error);
- //return apiReturn(c, false, "Error in setting the user access", error, 400);
- return c.json(
- {
- success: false,
- message: "Error in setting the user access",
- data: error,
- },
- 400
- );
+ responses: responses(),
+ }),
+ async (c) => {
+ //apiHit(c, { endpoint: "api/auth/setUserRoles" });
+ const userData = await c.req.json();
+ try {
+ const userUPD: any = await updateUserADM(userData);
+ //return apiReturn(c, true, access?.message, access?.data, 200);
+ return c.json(
+ {
+ success: userUPD.success,
+ message: userUPD.message,
+ data: userUPD.data,
+ },
+ 200
+ );
+ } catch (error) {
+ console.log(error);
+ //return apiReturn(c, false, "Error in setting the user access", error, 400);
+ return c.json(
+ {
+ success: false,
+ message: "Error in setting the user access",
+ data: error,
+ },
+ 400
+ );
+ }
}
- }
);
export default app;
diff --git a/server/services/notifications/utils/views/passwordReset.hbs b/server/services/notifications/utils/views/passwordReset.hbs
new file mode 100644
index 0000000..db7789d
--- /dev/null
+++ b/server/services/notifications/utils/views/passwordReset.hbs
@@ -0,0 +1,36 @@
+
+
+
+
+ {{!--Order Summary --}}
+ {{> styles}}
+
+ {{!-- --}}
+
+
+
+ Dear {{username}},
+
+ Your password was change. Please find your new temporary password below:
+
+ Temporary Password: {{password}}
+
+ For security reasons, we strongly recommend changing your password as soon as possible.
+
+ You can update it by logging into your account and navigating to the password settings section.
+
+ Best regards,
+ LST team
+
+
+
+
\ No newline at end of file