diff --git a/.includeControls b/.includeControls index 58e8ba5..d1d5d93 100644 --- a/.includeControls +++ b/.includeControls @@ -6,7 +6,7 @@ lstWrapper/publish/LstWrapper.exe lstWrapper/publish/LstWrapper.pdb lstWrapper/publish/LstWrapper.runtimeconfig.json lstWrapper/publish/LstWrapper.staticwebassets.endpoints.json -web.config +lstWrapper/publish/web.config controller/lst_ctl.exe controller/.env-example scripts/update-controller-bumpBuild.ps1 diff --git a/LogisticsSupportTool_API_DOCS/app/auth/Request Resetpassword.bru b/LogisticsSupportTool_API_DOCS/app/auth/Request Resetpassword.bru new file mode 100644 index 0000000..8c5f699 --- /dev/null +++ b/LogisticsSupportTool_API_DOCS/app/auth/Request Resetpassword.bru @@ -0,0 +1,21 @@ +meta { + name: Request Resetpassword + type: http + seq: 10 +} + +post { + url: {{url}}/lst/api/user/resetPassword + body: json + auth: inherit +} + +body:json { + { + "email": "blake.matthes@alpla.com", + } +} + +settings { + encodeUrl: true +} diff --git a/LogisticsSupportTool_API_DOCS/app/auth/Resetpassword.bru b/LogisticsSupportTool_API_DOCS/app/auth/Resetpassword.bru new file mode 100644 index 0000000..2ae8776 --- /dev/null +++ b/LogisticsSupportTool_API_DOCS/app/auth/Resetpassword.bru @@ -0,0 +1,25 @@ +meta { + name: Resetpassword + type: http + seq: 11 +} + +post { + url: http://localhost:4200/api/auth/reset-password/gCo7OUP6CH2Qu7obhvOrhuo9?callbackURL + body: json + auth: inherit +} + +params:query { + callbackURL: +} + +body:json { + { + "newPassword": "nova0511" + } +} + +settings { + encodeUrl: true +} diff --git a/app/src/internal/auth/routes/resetPassword.ts b/app/src/internal/auth/routes/resetPassword.ts new file mode 100644 index 0000000..89509c7 --- /dev/null +++ b/app/src/internal/auth/routes/resetPassword.ts @@ -0,0 +1,38 @@ +import { Router, type Request, type Response } from "express"; + +import { authClient } from "../../../pkg/auth/auth-client.js"; +import z from "zod"; +import { auth } from "../../../pkg/auth/auth.js"; + +const resetSchema = z.object({ + email: z.string(), +}); + +const router = Router(); + +router.post("/", async (req: Request, res: Response) => { + try { + const validated = resetSchema.parse(req.body); + + const data = await auth.api.requestPasswordReset({ + body: { + email: validated.email, + redirectTo: `${process.env.BETTER_AUTH_URL}/user/resetpassword`, + }, + }); + return res.json(data); + } catch (err) { + if (err instanceof z.ZodError) { + const flattened = z.flattenError(err); + return res.status(400).json({ + error: "Validation failed", + details: flattened, + }); + } + + console.log(err); + return res.status(400).json({ error: err }); + } +}); + +export default router; diff --git a/app/src/internal/auth/routes/routes.ts b/app/src/internal/auth/routes/routes.ts index b7758d4..b7bed18 100644 --- a/app/src/internal/auth/routes/routes.ts +++ b/app/src/internal/auth/routes/routes.ts @@ -2,10 +2,12 @@ import type { Express, Request, Response } from "express"; import me from "./me.js"; import register from "./register.js"; import userRoles from "./userroles.js"; +import resetPassword from "./resetPassword.js"; import { requireAuth } from "../../../pkg/middleware/authMiddleware.js"; export const setupAuthRoutes = (app: Express, basePath: string) => { app.use(basePath + "/api/user/me", requireAuth(), me); + app.use(basePath + "/api/user/resetpassword", resetPassword); app.use(basePath + "/api/user/register", register); app.use(basePath + "/api/user/roles", requireAuth(), userRoles); }; diff --git a/app/src/pkg/auth/auth-client.ts b/app/src/pkg/auth/auth-client.ts index fea07fc..4a90e1f 100644 --- a/app/src/pkg/auth/auth-client.ts +++ b/app/src/pkg/auth/auth-client.ts @@ -9,7 +9,7 @@ import { import type { auth } from "./auth.js"; export const authClient = createAuthClient({ - baseURL: "http://localhost:3000", + baseURL: process.env.BETTER_AUTH_URL, plugins: [ inferAdditionalFields(), usernameClient(), diff --git a/app/src/pkg/auth/auth.ts b/app/src/pkg/auth/auth.ts index 2ffc657..260bef6 100644 --- a/app/src/pkg/auth/auth.ts +++ b/app/src/pkg/auth/auth.ts @@ -5,6 +5,7 @@ import { betterAuth } from "better-auth"; import * as rawSchema from "../db/schema/auth-schema.js"; import type { User } from "better-auth/types"; import { eq } from "drizzle-orm"; +import { sendEmail } from "../utils/mail/sendMail.js"; export const schema = { user: rawSchema.user, @@ -14,6 +15,7 @@ export const schema = { jwks: rawSchema.jwks, apiKey: rawSchema.apikey, // 🔑 rename to apiKey }; +const RESET_EXPIRY_SECONDS = 3600; // 1 hour export const auth = betterAuth({ database: drizzleAdapter(db, { @@ -24,11 +26,40 @@ export const auth = betterAuth({ "*.alpla.net", "http://localhost:5173", "http://localhost:5500", + "http://localhost:4200", + "http://localhost:4000", ], appName: "lst", emailAndPassword: { enabled: true, minPasswordLength: 8, // optional config + resetPasswordTokenExpirySeconds: RESET_EXPIRY_SECONDS, // time in seconds + sendResetPassword: async ({ user, token }) => { + const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`; + const expiryMinutes = Math.floor(RESET_EXPIRY_SECONDS / 60); + const expiryText = + expiryMinutes >= 60 + ? `${expiryMinutes / 60} hour${ + expiryMinutes === 60 ? "" : "s" + }` + : `${expiryMinutes} minutes`; + const emailData = { + email: user.email, + subject: "LST- Forgot password request", + template: "forgotPassword", + context: { + username: user.name, + email: user.email, + url: frontendUrl, + expiry: expiryText, + }, + }; + await sendEmail(emailData); + }, + // onPasswordReset: async ({ user }, request) => { + // // your logic here + // console.log(`Password for user ${user.email} has been reset.`); + // }, }, plugins: [ //jwt({ jwt: { expirationTime: "1h" } }), diff --git a/app/src/pkg/middleware/apiHits.ts b/app/src/pkg/middleware/apiHits.ts index dd23774..cf60530 100644 --- a/app/src/pkg/middleware/apiHits.ts +++ b/app/src/pkg/middleware/apiHits.ts @@ -2,6 +2,24 @@ import type { Request, Response, NextFunction } from "express"; import { db } from "../db/db.js"; import { apiHits } from "../db/schema/apiHits.js"; +type StripPasswords = T extends Array + ? StripPasswords[] + : T extends object + ? { [K in keyof T as Exclude]: StripPasswords } + : T; + +function stripPasswords(input: T): StripPasswords { + if (Array.isArray(input)) { + return input.map(stripPasswords) as StripPasswords; + } else if (input && typeof input === "object") { + const entries = Object.entries(input as object) + .filter(([key]) => key !== "password") + .map(([key, value]) => [key, stripPasswords(value)]); + return Object.fromEntries(entries) as StripPasswords; + } + return input as StripPasswords; +} + export function apiHitMiddleware( req: Request, res: Response, @@ -15,7 +33,7 @@ export function apiHitMiddleware( await db.insert(apiHits).values({ method: req.method, path: req.originalUrl, - body: JSON.stringify(req.body ?? {}), + body: JSON.stringify(req.body ? stripPasswords(req.body) : {}), status: res.statusCode, ip: req.ip, duration: Date.now() - start, diff --git a/app/src/pkg/utils/mail/sendMail.ts b/app/src/pkg/utils/mail/sendMail.ts new file mode 100644 index 0000000..971136f --- /dev/null +++ b/app/src/pkg/utils/mail/sendMail.ts @@ -0,0 +1,123 @@ +import type { Address } from "nodemailer/lib/mailer/index.js"; +import type { Transporter } from "nodemailer"; +import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js"; +import type Mail from "nodemailer/lib/mailer/index.js"; +import os from "os"; +import nodemailer from "nodemailer"; +import path from "path"; +import { fileURLToPath } from "url"; +import { promisify } from "util"; +import hbs from "nodemailer-express-handlebars"; +import { createLogger } from "../../logger/logger.js"; + +interface HandlebarsMailOptions extends Mail.Options { + template: string; + context: Record; +} + +interface EmailData { + email: string; + subject: string; + template: string; + context: Record; +} + +export const sendEmail = async (data: EmailData): Promise => { + const log = createLogger({ module: "pkg", subModule: "sendMail" }); + let transporter: Transporter; + let fromEmail: string | Address; + + if ( + os.hostname().includes("OLP") && + process.env.EMAIL_USER && + process.env.EMAIL_PASSWORD + ) { + transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD, + }, + //debug: true, + }); + + // update the from email + fromEmail = process.env.EMAIL_USER; + } else { + // convert to the correct plant token. + + let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`; + + //const testServers = ["vms036", "VMS036"]; + + if (os.hostname().includes("VMS036")) { + host = "USMCD1-smtp.alpla.net"; + } + + // if (plantToken[0].value === "usiow2") { + // host = "USIOW1-smtp.alpla.net"; + // } + + transporter = nodemailer.createTransport({ + host: host, + port: 25, + rejectUnauthorized: false, + //secure: false, + // auth: { + // user: "alplaprod", + // pass: "obelix", + // }, + debug: true, + } as SMTPTransport.Options); + + // update the from email + fromEmail = `noreply@alpla.com`; + } + + // creating the handlbar options + const viewPath = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "./views/" + ); + + const handlebarOptions = { + viewEngine: { + extname: ".hbs", + //layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory + defaultLayout: "", // Specify the default layout + partialsDir: viewPath, + }, + viewPath: viewPath, + extName: ".hbs", // File extension for Handlebars templates + }; + + transporter.use("compile", hbs(handlebarOptions)); + + const mailOptions: HandlebarsMailOptions = { + from: fromEmail, + to: data.email, + subject: data.subject, + //text: "You will have a reset token here and only have 30min to click the link before it expires.", + //html: emailTemplate("BlakesTest", "This is an example with css"), + template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs') + context: data.context, + }; + + // now verify and send the email + const sendMailPromise = promisify(transporter.sendMail).bind(transporter); + + try { + // Send email and await the result + const info = await sendMailPromise(mailOptions); + log.info(null, `Email was sent to: ${data.email}`); + return { success: true, message: "Email sent.", data: info }; + } catch (err) { + console.log(err); + log.error( + { error: err }, + + `Error sending Email to : ${data.email}` + ); + return { success: false, message: "Error sending email.", error: err }; + } +}; diff --git a/app/src/pkg/utils/mail/views/forgotPassword.hbs b/app/src/pkg/utils/mail/views/forgotPassword.hbs new file mode 100644 index 0000000..922e306 --- /dev/null +++ b/app/src/pkg/utils/mail/views/forgotPassword.hbs @@ -0,0 +1,69 @@ + + + + + Password Reset + {{> styles}} + + + + + + \ No newline at end of file diff --git a/app/src/pkg/utils/mail/views/styles.hbs b/app/src/pkg/utils/mail/views/styles.hbs new file mode 100644 index 0000000..09e1061 --- /dev/null +++ b/app/src/pkg/utils/mail/views/styles.hbs @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/controller/index.html b/controller/index.html index 45301be..04ac2bb 100644 --- a/controller/index.html +++ b/controller/index.html @@ -79,6 +79,7 @@ + @@ -266,6 +267,14 @@ }); // "frontend" = payload target logMessage("info", "Copying to USDAY1VMS006"); }; + document.getElementById("USMCD1VMS006").onclick = () => { + socket.emit("update", { + action: "copy", + target: "USMCD1VMS006", + drive: "e", + }); // "frontend" = payload target + logMessage("info", "Copying to USMCD1VMS006"); + }; socket.on("logs", (msg) => logMessage("logs", msg)); socket.on("errors", (msg) => logMessage("errors", msg)); diff --git a/frontend/src/components/navBar/Admin.tsx b/frontend/src/components/navBar/Admin.tsx index 17485b5..8d4633c 100644 --- a/frontend/src/components/navBar/Admin.tsx +++ b/frontend/src/components/navBar/Admin.tsx @@ -1,4 +1,5 @@ import { Server, Settings, User, type LucideIcon } from "lucide-react"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { userAccess, type UserRoles } from "../../lib/authClient"; import { SidebarGroup, @@ -65,6 +66,10 @@ export default function Admin() { ))} + + {userAccess(null, ["systemAdmin"]) && ( + + )} diff --git a/frontend/src/components/navBar/Nav.tsx b/frontend/src/components/navBar/Nav.tsx index ef13e81..0d1d574 100644 --- a/frontend/src/components/navBar/Nav.tsx +++ b/frontend/src/components/navBar/Nav.tsx @@ -21,16 +21,17 @@ export default function Nav() {
- {/*
- {settings.length > 0 && ( + */} + +
+ {session ? (
diff --git a/frontend/src/components/navBar/SideBarNav.tsx b/frontend/src/components/navBar/SideBarNav.tsx index 5b0d3a2..e48e76c 100644 --- a/frontend/src/components/navBar/SideBarNav.tsx +++ b/frontend/src/components/navBar/SideBarNav.tsx @@ -1,4 +1,9 @@ -import { Sidebar, SidebarFooter, SidebarTrigger } from "../ui/sidebar"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarTrigger, +} from "../ui/sidebar"; import { Header } from "./Header"; import Admin from "./Admin"; import { userAccess } from "../../lib/authClient"; @@ -8,7 +13,9 @@ export default function SideBarNav() {
- {userAccess(null, ["systemAdmin", "admin"]) && } + + {userAccess(null, ["systemAdmin", "admin"]) && } + diff --git a/frontend/src/lib/formStuff/components/InputPasswordField.tsx b/frontend/src/lib/formStuff/components/InputPasswordField.tsx new file mode 100644 index 0000000..4af3754 --- /dev/null +++ b/frontend/src/lib/formStuff/components/InputPasswordField.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { useFieldContext } from ".."; +import { FieldErrors } from "./FieldErrors"; +import { Eye, EyeOff } from "lucide-react"; +import { Label } from "../../../components/ui/label"; +import { Input } from "../../../components/ui/input"; +import { Button } from "../../../components/ui/button"; + +type PasswordInputProps = { + label: string; + required?: boolean; +}; + +export const InputPasswordField = ({ + label, + required = false, +}: PasswordInputProps) => { + const field = useFieldContext(); + const [show, setShow] = useState(false); + + return ( +
+ +
+ field.handleChange(e.target.value)} + onBlur={field.handleBlur} + required={required} + className="pr-10" + /> + +
+ +
+ ); +}; diff --git a/frontend/src/lib/formStuff/index.tsx b/frontend/src/lib/formStuff/index.tsx index 8972e0c..83d92ee 100644 --- a/frontend/src/lib/formStuff/index.tsx +++ b/frontend/src/lib/formStuff/index.tsx @@ -3,12 +3,18 @@ import { SubmitButton } from "./components/SubmitButton"; import { InputField } from "./components/InputField"; import { SelectField } from "./components/SelectField"; import { CheckboxField } from "./components/CheckBox"; +import { InputPasswordField } from "./components/InputPasswordField"; export const { fieldContext, useFieldContext, formContext, useFormContext } = createFormHookContexts(); export const { useAppForm } = createFormHook({ - fieldComponents: { InputField, SelectField, CheckboxField }, + fieldComponents: { + InputField, + InputPasswordField, + SelectField, + CheckboxField, + }, formComponents: { SubmitButton }, fieldContext, formContext, diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 9adb598..211b88b 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -15,6 +15,8 @@ import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as AdminLayoutAdminUsersRouteImport } from './routes/_adminLayout/admin/users' import { Route as AdminLayoutAdminSettingsRouteImport } from './routes/_adminLayout/admin/settings' import { Route as AdminLayoutAdminServersRouteImport } from './routes/_adminLayout/admin/servers' +import { Route as authUserSignupRouteImport } from './routes/(auth)/user/signup' +import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user/resetpassword' const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({ id: '/_adminLayout', @@ -46,10 +48,22 @@ const AdminLayoutAdminServersRoute = AdminLayoutAdminServersRouteImport.update({ path: '/admin/servers', getParentRoute: () => AdminLayoutRouteRoute, } as any) +const authUserSignupRoute = authUserSignupRouteImport.update({ + id: '/(auth)/user/signup', + path: '/user/signup', + getParentRoute: () => rootRouteImport, +} as any) +const authUserResetpasswordRoute = authUserResetpasswordRouteImport.update({ + id: '/(auth)/user/resetpassword', + path: '/user/resetpassword', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof authLoginRoute + '/user/resetpassword': typeof authUserResetpasswordRoute + '/user/signup': typeof authUserSignupRoute '/admin/servers': typeof AdminLayoutAdminServersRoute '/admin/settings': typeof AdminLayoutAdminSettingsRoute '/admin/users': typeof AdminLayoutAdminUsersRoute @@ -57,6 +71,8 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof authLoginRoute + '/user/resetpassword': typeof authUserResetpasswordRoute + '/user/signup': typeof authUserSignupRoute '/admin/servers': typeof AdminLayoutAdminServersRoute '/admin/settings': typeof AdminLayoutAdminSettingsRoute '/admin/users': typeof AdminLayoutAdminUsersRoute @@ -66,6 +82,8 @@ export interface FileRoutesById { '/': typeof IndexRoute '/_adminLayout': typeof AdminLayoutRouteRouteWithChildren '/(auth)/login': typeof authLoginRoute + '/(auth)/user/resetpassword': typeof authUserResetpasswordRoute + '/(auth)/user/signup': typeof authUserSignupRoute '/_adminLayout/admin/servers': typeof AdminLayoutAdminServersRoute '/_adminLayout/admin/settings': typeof AdminLayoutAdminSettingsRoute '/_adminLayout/admin/users': typeof AdminLayoutAdminUsersRoute @@ -75,16 +93,27 @@ export interface FileRouteTypes { fullPaths: | '/' | '/login' + | '/user/resetpassword' + | '/user/signup' | '/admin/servers' | '/admin/settings' | '/admin/users' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/admin/servers' | '/admin/settings' | '/admin/users' + to: + | '/' + | '/login' + | '/user/resetpassword' + | '/user/signup' + | '/admin/servers' + | '/admin/settings' + | '/admin/users' id: | '__root__' | '/' | '/_adminLayout' | '/(auth)/login' + | '/(auth)/user/resetpassword' + | '/(auth)/user/signup' | '/_adminLayout/admin/servers' | '/_adminLayout/admin/settings' | '/_adminLayout/admin/users' @@ -94,6 +123,8 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren authLoginRoute: typeof authLoginRoute + authUserResetpasswordRoute: typeof authUserResetpasswordRoute + authUserSignupRoute: typeof authUserSignupRoute } declare module '@tanstack/react-router' { @@ -140,6 +171,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AdminLayoutAdminServersRouteImport parentRoute: typeof AdminLayoutRouteRoute } + '/(auth)/user/signup': { + id: '/(auth)/user/signup' + path: '/user/signup' + fullPath: '/user/signup' + preLoaderRoute: typeof authUserSignupRouteImport + parentRoute: typeof rootRouteImport + } + '/(auth)/user/resetpassword': { + id: '/(auth)/user/resetpassword' + path: '/user/resetpassword' + fullPath: '/user/resetpassword' + preLoaderRoute: typeof authUserResetpasswordRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -162,6 +207,8 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren, authLoginRoute: authLoginRoute, + authUserResetpasswordRoute: authUserResetpasswordRoute, + authUserSignupRoute: authUserSignupRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/frontend/src/routes/(auth)/-components/LoginForm.tsx b/frontend/src/routes/(auth)/-components/LoginForm.tsx index 5c9dce0..47ef530 100644 --- a/frontend/src/routes/(auth)/-components/LoginForm.tsx +++ b/frontend/src/routes/(auth)/-components/LoginForm.tsx @@ -46,6 +46,7 @@ export default function LoginForm() { const session = await getSession(); setSession(session); + form.reset(); fetchRoles(); toast.success( @@ -54,12 +55,13 @@ export default function LoginForm() { router.invalidate(); router.history.push(search.redirect ? search.redirect : "/"); } catch (error) { - console.log(error); + // @ts-ignore + toast.error(error?.message); } }, }); return ( -
+
Login to your account @@ -87,9 +89,8 @@ export default function LoginForm() { ( - )} @@ -102,7 +103,7 @@ export default function LoginForm() { )} /> Forgot your password? @@ -118,9 +119,12 @@ export default function LoginForm() {
Don't have an account?{" "} - + Sign up - +
diff --git a/frontend/src/routes/(auth)/-components/RequestResetPassword.tsx b/frontend/src/routes/(auth)/-components/RequestResetPassword.tsx new file mode 100644 index 0000000..4b625ba --- /dev/null +++ b/frontend/src/routes/(auth)/-components/RequestResetPassword.tsx @@ -0,0 +1,88 @@ +import { LstCard } from "../../../components/ui/lstCard"; +import { + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/card"; +import { useAppForm } from "../../../lib/formStuff"; +import { api } from "../../../lib/axiosAPI"; +import { toast } from "sonner"; +import { Link } from "@tanstack/react-router"; + +export default function RequestResetPassword() { + const form = useAppForm({ + defaultValues: { + email: "", + }, + onSubmit: async ({ value }) => { + try { + const res = await api.post("api/user/resetpassword", { + email: value.email, + }); + + console.log(res); + + if (res.status === 200) { + toast.success( + res.data.message + ? res.data.message + : "If this email exists in our system, check your email for the reset link" + ); + } + } catch (error) { + console.log(error); + } + }, + }); + + return ( +
+ + + Reset your password + + Enter your email address and we’ll send you a reset link + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + ( + + )} + /> + +
+ + + Send Reset Link + + +
+ + +
+ Remembered your password?{" "} + + Back to login + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/(auth)/-components/ResetPasswordForm.tsx b/frontend/src/routes/(auth)/-components/ResetPasswordForm.tsx new file mode 100644 index 0000000..b7351a7 --- /dev/null +++ b/frontend/src/routes/(auth)/-components/ResetPasswordForm.tsx @@ -0,0 +1,114 @@ +import { useAppForm } from "../../../lib/formStuff"; +import { LstCard } from "../../../components/ui/lstCard"; +import { + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/card"; +import { api } from "../../../lib/axiosAPI"; +import { toast } from "sonner"; +import { Link, useNavigate } from "@tanstack/react-router"; + +export default function ResetPasswordForm({ token }: { token: string }) { + const navigate = useNavigate(); + const form = useAppForm({ + defaultValues: { + password: "", + confirmPassword: "", + }, + onSubmit: async ({ value }) => { + if (value.password != value.confirmPassword) { + toast.error("Passwords do not match"); + return; + } + try { + const res = await api.post("/api/auth/reset-password", { + newPassword: value.password, + token: token, + }); + if (res.status === 200) { + toast.success("Password has been reset"); + form.reset(); + navigate({ to: "/login" }); + } + } catch (error) { + console.log(error); + // @ts-ignore + toast.error(error?.response.data.message); + } + }, + }); + + return ( +
+ + + Set a new password + + Enter your new password below and confirm it to continue + + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + ( + + )} + /> + + { + // if ( + // value !== + // fieldApi.form.getFieldValue("password") + // ) { + // return "Passwords do not match"; + // } + // return undefined; + // }, + // }} + children={(field) => ( + + )} + /> + +
+ + + Reset Password + + +
+ + +
+ Remembered your account?{" "} + + Back to login + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/(auth)/-components/SignupForm.tsx b/frontend/src/routes/(auth)/-components/SignupForm.tsx new file mode 100644 index 0000000..8be6667 --- /dev/null +++ b/frontend/src/routes/(auth)/-components/SignupForm.tsx @@ -0,0 +1,129 @@ +import { toast } from "sonner"; +import { + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/card"; +import { LstCard } from "../../../components/ui/lstCard"; +import { api } from "../../../lib/axiosAPI"; +import { useAppForm } from "../../../lib/formStuff"; +import { Link } from "@tanstack/react-router"; + +export default function SignupForm() { + const form = useAppForm({ + defaultValues: { + username: "", + email: "", + password: "", + confirmPassword: "", + }, + onSubmit: async ({ value }) => { + if (value.password != value.confirmPassword) { + toast.error("Passwords do not match"); + return; + } + try { + const res = await api.post("/api/user/register", { + username: value.username, + name: value.username, + email: value.email, + password: value.password, + }); + + if (res.status === 200) { + toast.success(`Welcome ${value.username}, to lst.`); + } + } catch (error) { + console.log(error); + // @ts-ignore + toast.error(error?.response.data.message); + } + }, + }); + return ( +
+ + + Create an account + + Fill in your details to get started + + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + {/* Username */} + ( + + )} + /> + + {/* Email */} + ( + + )} + /> + + {/* Password */} + ( + + )} + /> + + {/* Confirm Password */} + ( + + )} + /> + +
+ + Sign Up + +
+ + +
+ Already have an account?{" "} + + Log in + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/(auth)/login.tsx b/frontend/src/routes/(auth)/login.tsx index 8e0092a..3aa2c9c 100644 --- a/frontend/src/routes/(auth)/login.tsx +++ b/frontend/src/routes/(auth)/login.tsx @@ -23,7 +23,7 @@ export const Route = createFileRoute("/(auth)/login")({ function RouteComponent() { return ( -
+
); diff --git a/frontend/src/routes/(auth)/user/resetpassword.tsx b/frontend/src/routes/(auth)/user/resetpassword.tsx new file mode 100644 index 0000000..45bcb9c --- /dev/null +++ b/frontend/src/routes/(auth)/user/resetpassword.tsx @@ -0,0 +1,27 @@ +import { createFileRoute } from "@tanstack/react-router"; +import z from "zod"; +import RequestResetPassword from "../-components/RequestResetPassword"; +import ResetPasswordForm from "../-components/ResetPasswordForm"; + +export const Route = createFileRoute("/(auth)/user/resetpassword")({ + // beforeLoad: ({ search }) => { + // return { token: search.token }; + // }, + validateSearch: z.object({ + token: z.string().optional(), + }), + component: RouteComponent, +}); + +function RouteComponent() { + const { token } = Route.useSearch(); + return ( +
+ {token ? ( + + ) : ( + + )} +
+ ); +} diff --git a/frontend/src/routes/(auth)/user/signup.tsx b/frontend/src/routes/(auth)/user/signup.tsx new file mode 100644 index 0000000..b0d734a --- /dev/null +++ b/frontend/src/routes/(auth)/user/signup.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import SignupForm from "../-components/SignupForm"; + +export const Route = createFileRoute("/(auth)/user/signup")({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( +
+ +
+ ); +} diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 9216a76..6f1c82e 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,5 +1,5 @@ import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; + import type { QueryClient } from "@tanstack/react-query"; import { Toaster } from "sonner"; import Cookies from "js-cookie"; @@ -18,7 +18,6 @@ interface RootRouteContext { const RootLayout = () => { //const { logout, login } = Route.useRouteContext(); - const defaultOpen = Cookies.get("sidebar_state") === "true"; return (
@@ -26,16 +25,15 @@ const RootLayout = () => {