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 ( +
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + + value.length > 3 + ? undefined + : "Username must be longer than 3 letters", + }} + children={(field) => { + return ( +
+ + + 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 ( +
+ + + 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 ( +
+ +
+ + field.handleChange(e.target.value) + } + /> + +
+ {field.state.meta.errors.length ? ( + {field.state.meta.errors.join(",")} + ) : null} +
+ ); + }} + /> + +
+ +
+
+ ); +} 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