feat(auth): admin user updates added
if a password change happens then an email will be sent to the user.
This commit is contained in:
43
frontend/src/components/admin/user/UserPage.tsx
Normal file
43
frontend/src/components/admin/user/UserPage.tsx
Normal file
@@ -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 <div className="m-auto">Loading users...</div>;
|
||||||
|
|
||||||
|
if (isError)
|
||||||
|
return (
|
||||||
|
<div className="m-auto">
|
||||||
|
There was an error getting the users.... {JSON.stringify(error)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-2 w-dvw">
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
{data.map((u: any) => {
|
||||||
|
return (
|
||||||
|
<AccordionItem key={u.user_id} value={u.user_id}>
|
||||||
|
<AccordionTrigger>
|
||||||
|
<span>{u.username}</span>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div>
|
||||||
|
<UserCard user={u} />
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
frontend/src/components/admin/user/components/UserCard.tsx
Normal file
183
frontend/src/components/admin/user/components/UserCard.tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.Field
|
||||||
|
name="username"
|
||||||
|
validators={{
|
||||||
|
// We can choose between form-wide and field-specific validators
|
||||||
|
onChange: ({ value }) =>
|
||||||
|
value.length > 3
|
||||||
|
? undefined
|
||||||
|
: "Username must be longer than 3 letters",
|
||||||
|
}}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
//type="number"
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.length ? (
|
||||||
|
<em>{field.state.meta.errors.join(",")}</em>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<form.Field
|
||||||
|
name="email"
|
||||||
|
validators={{
|
||||||
|
// We can choose between form-wide and field-specific validators
|
||||||
|
onChange: ({ value }) =>
|
||||||
|
value.length > 3
|
||||||
|
? undefined
|
||||||
|
: "You must enter a correct ",
|
||||||
|
}}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
//type="number"
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.length ? (
|
||||||
|
<em>{field.state.meta.errors.join(",")}</em>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<form.Field
|
||||||
|
name="password"
|
||||||
|
validators={{
|
||||||
|
onChangeAsyncDebounceMs: 500,
|
||||||
|
onChangeAsync: ({ value }) => {
|
||||||
|
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 (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2">
|
||||||
|
<Label htmlFor="password">
|
||||||
|
Change Password
|
||||||
|
</Label>
|
||||||
|
<div className="mt-2 flex flex-row">
|
||||||
|
<Input
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
//type="number"
|
||||||
|
onChange={(e) =>
|
||||||
|
field.handleChange(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="ml-2"
|
||||||
|
onClick={() =>
|
||||||
|
field.handleChange(
|
||||||
|
generatePassword(8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Random password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{field.state.meta.errors.length ? (
|
||||||
|
<em>{field.state.meta.errors.join(",")}</em>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
<Button onClick={form.handleSubmit}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
@@ -10,7 +20,11 @@ import {
|
|||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from "../../ui/sidebar";
|
} from "../../ui/sidebar";
|
||||||
import {Collapsible, CollapsibleContent, CollapsibleTrigger} from "../../ui/collapsible";
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "../../ui/collapsible";
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -53,9 +67,9 @@ const data = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Users",
|
title: "Users",
|
||||||
url: "#",
|
url: "/users",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
isActive: false,
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "UCD",
|
title: "UCD",
|
||||||
@@ -82,7 +96,11 @@ export function AdminSideBar() {
|
|||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{data.navMain.map((item, index) => (
|
{data.navMain.map((item, index) => (
|
||||||
<Collapsible key={item.title} defaultOpen={index === 1} className="group/collapsible">
|
<Collapsible
|
||||||
|
key={item.title}
|
||||||
|
defaultOpen={index === 1}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton>
|
<SidebarMenuButton>
|
||||||
@@ -96,15 +114,25 @@ export function AdminSideBar() {
|
|||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{item.items.map((item) => (
|
{item.items.map((item) => (
|
||||||
<SidebarMenuSubItem key={item.title}>
|
<SidebarMenuSubItem
|
||||||
|
key={item.title}
|
||||||
|
>
|
||||||
{item.isActive && (
|
{item.isActive && (
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={item.url}
|
href={item.url}
|
||||||
target={item.newWindow ? "_blank" : "_self"}
|
target={
|
||||||
|
item.newWindow
|
||||||
|
? "_blank"
|
||||||
|
: "_self"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
<span>{item.title}</span>
|
<span>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
64
frontend/src/components/ui/accordion.tsx
Normal file
64
frontend/src/components/ui/accordion.tsx
Normal file
@@ -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<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
10
frontend/src/routes/_admin/users.tsx
Normal file
10
frontend/src/routes/_admin/users.tsx
Normal file
@@ -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 <UserPage />;
|
||||||
|
}
|
||||||
13
frontend/src/utils/formStuff/options/userformOptions.tsx
Normal file
13
frontend/src/utils/formStuff/options/userformOptions.tsx
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
27
frontend/src/utils/passwordGen.ts
Normal file
27
frontend/src/utils/passwordGen.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
26
frontend/src/utils/querys/admin/users.tsx
Normal file
26
frontend/src/utils/querys/admin/users.tsx
Normal file
@@ -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 ?? [];
|
||||||
|
};
|
||||||
@@ -5,64 +5,78 @@ import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
|||||||
import type { User } from "../../../../types/users.js";
|
import type { User } from "../../../../types/users.js";
|
||||||
import { createPassword } from "../../utils/createPassword.js";
|
import { createPassword } from "../../utils/createPassword.js";
|
||||||
import { createLog } from "../../../logger/logger.js";
|
import { createLog } from "../../../logger/logger.js";
|
||||||
|
import { sendEmail } from "../../../notifications/controller/sendMail.js";
|
||||||
|
|
||||||
export const updateUserADM = async (userData: User) => {
|
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.
|
* The user model will need to be passed over so we can update per the request on the user.
|
||||||
* password, username, email.
|
* password, username, email.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
createLog(
|
createLog(
|
||||||
"info",
|
"info",
|
||||||
"apiAuthedRoute",
|
"apiAuthedRoute",
|
||||||
"auth",
|
"auth",
|
||||||
`${userData.user_id} is being updated.`
|
`${userData.user_id} is being updated.`
|
||||||
);
|
);
|
||||||
// get the orignal user info
|
// get the orignal user info
|
||||||
const { data: user, error: userError } = await tryCatch(
|
const { data: user, error: userError } = await tryCatch(
|
||||||
db.select().from(users).where(eq(users.user_id, userData.user_id!))
|
db.select().from(users).where(eq(users.user_id, userData.user_id!))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userError) {
|
if (userError) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "There was an error getting the user",
|
message: "There was an error getting the user",
|
||||||
userError,
|
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 {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
message:
|
message: `${userData.username} has been updated.`,
|
||||||
"The user you are looking for has either been deleted or dose not exist.",
|
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,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,82 +10,76 @@ import { updateUserADM } from "../../controllers/userAdmin/updateUserAdm.js";
|
|||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
const responseSchema = z.object({
|
const responseSchema = z.object({
|
||||||
success: z.boolean().openapi({ example: true }),
|
success: z.boolean().openapi({ example: true }),
|
||||||
message: z.string().optional().openapi({ example: "user access" }),
|
message: z.string().optional().openapi({ example: "user access" }),
|
||||||
data: z.array(z.object({})).optional().openapi({ example: [] }),
|
data: z.array(z.object({})).optional().openapi({ example: [] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserAccess = z.object({
|
const UserAccess = z.object({
|
||||||
user_id: z.string().openapi({ example: "users UUID" }),
|
user_id: z.string().openapi({ example: "users UUID" }),
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[a-zA-Z0-9_]{3,30}$/)
|
.regex(/^[a-zA-Z0-9_]{3,30}$/)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({ example: "smith034" }),
|
.openapi({ example: "smith034" }),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.email()
|
.email()
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({ example: "smith@example.com" }),
|
.openapi({ example: "smith@example.com" }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(6, { message: "Passwords must be longer than 3 characters" })
|
|
||||||
.regex(/[A-Z]/, {
|
.optional()
|
||||||
message: "Password must contain at least one uppercase letter",
|
.openapi({ example: "Password1!" }),
|
||||||
})
|
|
||||||
.regex(/[\W_]/, {
|
|
||||||
message: "Password must contain at least one special character",
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.openapi({ example: "Password1!" }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.openapi(
|
app.openapi(
|
||||||
createRoute({
|
createRoute({
|
||||||
tags: ["Auth:admin"],
|
tags: ["Auth:admin"],
|
||||||
summary: "updates a specific user",
|
summary: "updates a specific user",
|
||||||
method: "post",
|
method: "patch",
|
||||||
path: "/updateuser",
|
path: "/updateuser",
|
||||||
middleware: [
|
middleware: [
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
hasCorrectRole(["admin", "systemAdmin"], "admin"),
|
hasCorrectRole(["admin", "systemAdmin"], "admin"),
|
||||||
],
|
],
|
||||||
//description: "When logged in you will be able to grant new permissions",
|
//description: "When logged in you will be able to grant new permissions",
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": { schema: UserAccess },
|
"application/json": { schema: UserAccess },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
responses: responses(),
|
||||||
},
|
}),
|
||||||
responses: responses(),
|
async (c) => {
|
||||||
}),
|
//apiHit(c, { endpoint: "api/auth/setUserRoles" });
|
||||||
async (c) => {
|
const userData = await c.req.json();
|
||||||
//apiHit(c, { endpoint: "api/auth/setUserRoles" });
|
try {
|
||||||
const userData = await c.req.json();
|
const userUPD: any = await updateUserADM(userData);
|
||||||
try {
|
//return apiReturn(c, true, access?.message, access?.data, 200);
|
||||||
const userUPD: any = await updateUserADM(userData);
|
return c.json(
|
||||||
//return apiReturn(c, true, access?.message, access?.data, 200);
|
{
|
||||||
return c.json(
|
success: userUPD.success,
|
||||||
{
|
message: userUPD.message,
|
||||||
success: userUPD.success,
|
data: userUPD.data,
|
||||||
message: userUPD.message,
|
},
|
||||||
data: userUPD.data,
|
200
|
||||||
},
|
);
|
||||||
200
|
} catch (error) {
|
||||||
);
|
console.log(error);
|
||||||
} catch (error) {
|
//return apiReturn(c, false, "Error in setting the user access", error, 400);
|
||||||
console.log(error);
|
return c.json(
|
||||||
//return apiReturn(c, false, "Error in setting the user access", error, 400);
|
{
|
||||||
return c.json(
|
success: false,
|
||||||
{
|
message: "Error in setting the user access",
|
||||||
success: false,
|
data: error,
|
||||||
message: "Error in setting the user access",
|
},
|
||||||
data: error,
|
400
|
||||||
},
|
);
|
||||||
400
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
36
server/services/notifications/utils/views/passwordReset.hbs
Normal file
36
server/services/notifications/utils/views/passwordReset.hbs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
{{!--<title>Order Summary</title> --}}
|
||||||
|
{{> styles}}
|
||||||
|
<style>
|
||||||
|
pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #d63384;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
Dear {{username}},<br/><br/>
|
||||||
|
|
||||||
|
Your password was change. Please find your new temporary password below:<br/><br/>
|
||||||
|
|
||||||
|
Temporary Password: <em><b>{{password}}</b></em><br/><br/>
|
||||||
|
|
||||||
|
For security reasons, we strongly recommend changing your password as soon as possible.<br/><br/>
|
||||||
|
|
||||||
|
You can update it by logging into your account and navigating to the password settings section.<br/><br/>
|
||||||
|
|
||||||
|
Best regards,<br/><br/>
|
||||||
|
LST team<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user