diff --git a/backend/socket.io/serverSetup.ts b/backend/socket.io/serverSetup.ts index 893cd13..dba1ef3 100644 --- a/backend/socket.io/serverSetup.ts +++ b/backend/socket.io/serverSetup.ts @@ -3,7 +3,7 @@ import type { Server as HttpServer } from "node:http"; //import { fileURLToPath } from "node:url"; import { instrument } from "@socket.io/admin-ui"; import { Server } from "socket.io"; -import { auth } from "utils/auth.utils.js"; + import { createLogger } from "../logger/logger.controller.js"; import { allowedOrigins } from "../utils/cors.utils.js"; import { registerEmitter } from "./roomEmitter.socket.js"; @@ -15,6 +15,7 @@ const log = createLogger({ module: "socket.io", subModule: "setup" }); //import type { Session, User } from "better-auth"; // adjust if needed import { protectedRooms } from "./roomDefinitions.socket.js"; +import { auth } from "../utils/auth.utils.js"; // declare module "socket.io" { // interface Socket { diff --git a/backend/utils/auth.utils.ts b/backend/utils/auth.utils.ts index abcbeac..7a3cbd1 100644 --- a/backend/utils/auth.utils.ts +++ b/backend/utils/auth.utils.ts @@ -77,7 +77,7 @@ export const auth = betterAuth({ minPasswordLength: 8, // optional config resetPasswordTokenExpirySeconds: process.env.RESET_EXPIRY_SECONDS, // time in seconds sendResetPassword: async ({ user, token }) => { - const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`; + const frontendUrl = `${process.env.URL}/lst/app/user/resetpassword?token=${token}`; const expiryMinutes = Math.floor( parseInt(process.env.RESET_EXPIRY_SECONDS ?? "3600", 10) / 60, ); @@ -137,5 +137,3 @@ export const auth = betterAuth({ // }, }, }); - -type Session = typeof auth.$Infer.Session; diff --git a/backend/utils/sendEmail.utils.ts b/backend/utils/sendEmail.utils.ts index f094f0d..274cae5 100644 --- a/backend/utils/sendEmail.utils.ts +++ b/backend/utils/sendEmail.utils.ts @@ -1,4 +1,3 @@ -import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -6,7 +5,6 @@ import type { Transporter } from "nodemailer"; import nodemailer from "nodemailer"; import type Mail from "nodemailer/lib/mailer/index.js"; import type { Address } from "nodemailer/lib/mailer/index.js"; -import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js"; import hbs from "nodemailer-express-handlebars"; import { returnFunc } from "./returnHelper.utils.js"; import { tryCatch } from "./trycatch.utils.js"; @@ -24,62 +22,36 @@ interface EmailData { } export const sendEmail = async (data: EmailData) => { - let transporter: Transporter; - let fromEmail: string | Address; + const fromEmail: string = `DoNotReply@mail.alpla.com`; + const transporter: Transporter = nodemailer.createTransport({ + host: "smtp.azurecomm.net", + port: 587, + //rejectUnauthorized: false, + tls: { + minVersion: "TLSv1.2", + }, + auth: { + user: "donotreply@mail.alpla.com", + pass: process.env.SMTP_PASSWORD, + }, + debug: true, + }); - if (os.hostname().includes("OLP")) { - transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD, // The 16-character App Password - }, - //debug: true, - }); - - // update the from email - fromEmail = process.env.EMAIL_USER as string; - - // update the from email - fromEmail = `donotreply@alpla.com`; - } else { - //create the servers smtp config - let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`; - - if (os.hostname().includes("VMS036")) { - host = "USMCD1-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 = `donotreply@alpla.com`; - } - - // create the handlebars view + // creating the handlbar options const viewPath = path.resolve( path.dirname(fileURLToPath(import.meta.url)), - "./mailViews", + "../utils/mailViews/", ); const handlebarOptions = { viewEngine: { extname: ".hbs", - defaultLayout: "", + //layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory + defaultLayout: "", // Specify the default layout partialsDir: viewPath, }, viewPath: viewPath, - extName: ".hbs", + extName: ".hbs", // File extension for Handlebars templates }; transporter.use("compile", hbs(handlebarOptions)); @@ -89,7 +61,7 @@ export const sendEmail = async (data: EmailData) => { 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("Blake's Test", "This is an example with css"), + //html: emailTemplate("BlakesTest", "This is an example with css"), template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs') context: data.context, }; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index f3a8b7c..48b7835 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -69,7 +69,7 @@ export default function Header() { - Profile + Profile {/* Billing diff --git a/frontend/src/components/ui/spinner.tsx b/frontend/src/components/ui/spinner.tsx new file mode 100644 index 0000000..91f6a63 --- /dev/null +++ b/frontend/src/components/ui/spinner.tsx @@ -0,0 +1,10 @@ +import { cn } from "@/lib/utils" +import { Loader2Icon } from "lucide-react" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/frontend/src/lib/formSutff/Input.Field.tsx b/frontend/src/lib/formSutff/Input.Field.tsx index bdbb203..5c39826 100644 --- a/frontend/src/lib/formSutff/Input.Field.tsx +++ b/frontend/src/lib/formSutff/Input.Field.tsx @@ -20,7 +20,7 @@ export const InputField = ({ label, inputType, required }: InputFieldProps) => { const field = useFieldContext(); return ( -
+
{ return (
); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index b721a60..a81e988 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as AdminLogsRouteImport } from './routes/admin/logs' import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup' import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword' +import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile' const AboutRoute = AboutRouteImport.update({ id: '/about', @@ -46,12 +47,18 @@ const authUserResetpasswordRoute = authUserResetpasswordRouteImport.update({ path: '/user/resetpassword', getParentRoute: () => rootRouteImport, } as any) +const authUserProfileRoute = authUserProfileRouteImport.update({ + id: '/(auth)/user/profile', + path: '/user/profile', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/about': typeof AboutRoute '/login': typeof authLoginRoute '/admin/logs': typeof AdminLogsRoute + '/user/profile': typeof authUserProfileRoute '/user/resetpassword': typeof authUserResetpasswordRoute '/user/signup': typeof authUserSignupRoute } @@ -60,6 +67,7 @@ export interface FileRoutesByTo { '/about': typeof AboutRoute '/login': typeof authLoginRoute '/admin/logs': typeof AdminLogsRoute + '/user/profile': typeof authUserProfileRoute '/user/resetpassword': typeof authUserResetpasswordRoute '/user/signup': typeof authUserSignupRoute } @@ -69,6 +77,7 @@ export interface FileRoutesById { '/about': typeof AboutRoute '/(auth)/login': typeof authLoginRoute '/admin/logs': typeof AdminLogsRoute + '/(auth)/user/profile': typeof authUserProfileRoute '/(auth)/user/resetpassword': typeof authUserResetpasswordRoute '/(auth)/user/signup': typeof authUserSignupRoute } @@ -79,6 +88,7 @@ export interface FileRouteTypes { | '/about' | '/login' | '/admin/logs' + | '/user/profile' | '/user/resetpassword' | '/user/signup' fileRoutesByTo: FileRoutesByTo @@ -87,6 +97,7 @@ export interface FileRouteTypes { | '/about' | '/login' | '/admin/logs' + | '/user/profile' | '/user/resetpassword' | '/user/signup' id: @@ -95,6 +106,7 @@ export interface FileRouteTypes { | '/about' | '/(auth)/login' | '/admin/logs' + | '/(auth)/user/profile' | '/(auth)/user/resetpassword' | '/(auth)/user/signup' fileRoutesById: FileRoutesById @@ -104,6 +116,7 @@ export interface RootRouteChildren { AboutRoute: typeof AboutRoute authLoginRoute: typeof authLoginRoute AdminLogsRoute: typeof AdminLogsRoute + authUserProfileRoute: typeof authUserProfileRoute authUserResetpasswordRoute: typeof authUserResetpasswordRoute authUserSignupRoute: typeof authUserSignupRoute } @@ -152,6 +165,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof authUserResetpasswordRouteImport parentRoute: typeof rootRouteImport } + '/(auth)/user/profile': { + id: '/(auth)/user/profile' + path: '/user/profile' + fullPath: '/user/profile' + preLoaderRoute: typeof authUserProfileRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -160,6 +180,7 @@ const rootRouteChildren: RootRouteChildren = { AboutRoute: AboutRoute, authLoginRoute: authLoginRoute, AdminLogsRoute: AdminLogsRoute, + authUserProfileRoute: authUserProfileRoute, authUserResetpasswordRoute: authUserResetpasswordRoute, authUserSignupRoute: authUserSignupRoute, } diff --git a/frontend/src/routes/(auth)/-components/ChangePassword.tsx b/frontend/src/routes/(auth)/-components/ChangePassword.tsx new file mode 100644 index 0000000..a691340 --- /dev/null +++ b/frontend/src/routes/(auth)/-components/ChangePassword.tsx @@ -0,0 +1,90 @@ +import { useRouter } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { authClient } from "@/lib/auth-client"; +import { useAppForm } from "@/lib/formSutff"; + +export default function ChangePassword() { + const router = useRouter(); + const form = useAppForm({ + defaultValues: { + currentPassword: "", + newPassword: "", + confirmPassword: "", + }, + onSubmit: async ({ value }) => { + if (value.newPassword !== value.confirmPassword) { + toast.error("Passwords do not match"); + return; + } + const { data, error } = await authClient.changePassword({ + newPassword: value.newPassword, + currentPassword: value.currentPassword, + revokeOtherSessions: true, + }); + + if (data) { + toast.success("Password has been updated"); + form.reset(); + router.invalidate(); + + //navigate({ to: "/login" }); + } + + if (error) { + toast.success(error.message); + } + }, + }); + return ( +
+ + + Change password + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + +
+ + Update Profile + +
+
+
+
+
+ ); +} diff --git a/frontend/src/routes/(auth)/-components/LoginForm.tsx b/frontend/src/routes/(auth)/-components/LoginForm.tsx index fed4b9d..dbf3766 100644 --- a/frontend/src/routes/(auth)/-components/LoginForm.tsx +++ b/frontend/src/routes/(auth)/-components/LoginForm.tsx @@ -4,7 +4,6 @@ import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; diff --git a/frontend/src/routes/(auth)/-components/ResetForm.tsx b/frontend/src/routes/(auth)/-components/ResetForm.tsx new file mode 100644 index 0000000..12632a5 --- /dev/null +++ b/frontend/src/routes/(auth)/-components/ResetForm.tsx @@ -0,0 +1,76 @@ +import { Link } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { authClient } from "@/lib/auth-client"; +import { useAppForm } from "@/lib/formSutff"; + +export default function ResetForm() { + const form = useAppForm({ + defaultValues: { + email: "", + }, + onSubmit: async ({ value }) => { + const { data, error } = await authClient.requestPasswordReset({ + email: value.email, + redirectTo: `${window.location.origin}`, + }); + + if (data) { + toast.success(data.message); + } + + if (error) { + toast.error(error.message); + } + }, + }); + + return ( +
+ + + Reset your password + + Enter your email address and we’ll send you a reset link + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + + )} + + +
+ + Send Reset Link + +
+
+ +
+ Remembered your password?{" "} + + Back to login + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/(auth)/-components/ResetPassword.tsx b/frontend/src/routes/(auth)/-components/ResetPassword.tsx new file mode 100644 index 0000000..35f3cdb --- /dev/null +++ b/frontend/src/routes/(auth)/-components/ResetPassword.tsx @@ -0,0 +1,94 @@ +import { Link } from "@tanstack/react-router"; +import React from "react"; +import { toast } from "sonner"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { authClient } from "@/lib/auth-client"; +import { useAppForm } from "@/lib/formSutff"; + +export default function ResetPassword({ token }: { token: string }) { + const form = useAppForm({ + defaultValues: { + password: "", + confirmPassword: "", + }, + onSubmit: async ({ value }) => { + if (!token) { + toast.error("No token was included"); + return; + } + + const { data, error } = await authClient.resetPassword({ + newPassword: value.password, + token, + }); + + if (data) { + toast.success("Password has been reset"); + } + + if (error) { + toast.error(error.message); + } + }, + }); + + return ( +
+ + + Reset your password + + Enter your email address and we’ll send you a reset link + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + + )} + + + {(field) => ( + + )} + + +
+ + Send Reset Link + +
+
+ +
+ Remembered your password?{" "} + + Back to login + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/(auth)/user.profile.tsx b/frontend/src/routes/(auth)/user.profile.tsx new file mode 100644 index 0000000..b5154ef --- /dev/null +++ b/frontend/src/routes/(auth)/user.profile.tsx @@ -0,0 +1,99 @@ +import { createFileRoute, redirect, useRouter } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { authClient, useSession } from "@/lib/auth-client"; +import { useAppForm } from "@/lib/formSutff"; +import ChangePassword from "./-components/ChangePassword"; + +export const Route = createFileRoute("/(auth)/user/profile")({ + beforeLoad: async () => { + const result = await authClient.getSession({ + query: { disableCookieCache: true }, // force DB/Server lookup + }); + + //console.log("session check:", result.data); + + if (!result.data) { + throw redirect({ + to: "/login", + search: { + redirect: location.pathname + location.search, + }, + }); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + const { data: session } = useSession(); + const router = useRouter(); + const form = useAppForm({ + defaultValues: { + name: session?.user.name, + }, + onSubmit: async ({ value }) => { + const { data, error } = await authClient.updateUser({ + name: value.name, + }); + + if (data) { + toast.success("Profile has been updated"); + form.reset(); + //navigate({ to: "/login" }); + } + + if (error) { + toast.success(error.message); + } + }, + }); + return ( +
+
+ + + Profile + + Change your profile and password below + + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + + )} + + +
+ + Update Profile + +
+
+
+
+
+
+ +
+
+ ); +} diff --git a/frontend/src/routes/(auth)/user.resetpassword.tsx b/frontend/src/routes/(auth)/user.resetpassword.tsx index 14b6a40..c2534bf 100644 --- a/frontend/src/routes/(auth)/user.resetpassword.tsx +++ b/frontend/src/routes/(auth)/user.resetpassword.tsx @@ -1,9 +1,31 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, Link } from "@tanstack/react-router"; +import { toast } from "sonner"; +import z from "zod"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { authClient } from "@/lib/auth-client"; +import { useAppForm } from "@/lib/formSutff"; +import ResetForm from "./-components/ResetForm"; +import ResetPassword from "./-components/ResetPassword"; -export const Route = createFileRoute('/(auth)/user/resetpassword')({ - component: RouteComponent, -}) +export const Route = createFileRoute("/(auth)/user/resetpassword")({ + validateSearch: z.object({ + token: z.string().optional(), + }), + component: RouteComponent, +}); function RouteComponent() { - return
Hello "/(auth)/user/resetpassword"!
+ const { token } = Route.useSearch(); + + return ( +
+ {token ? : } +
+ ); } diff --git a/frontend/src/routes/(auth)/user.signup.tsx b/frontend/src/routes/(auth)/user.signup.tsx index faeca71..fdb60fa 100644 --- a/frontend/src/routes/(auth)/user.signup.tsx +++ b/frontend/src/routes/(auth)/user.signup.tsx @@ -1,9 +1,128 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { authClient } from "@/lib/auth-client"; +import { useAppForm } from "@/lib/formSutff"; -export const Route = createFileRoute('/(auth)/user/signup')({ - component: RouteComponent, -}) +export const Route = createFileRoute("/(auth)/user/signup")({ + component: RouteComponent, +}); function RouteComponent() { - return
Hello "/(auth)/user/signup"!
+ const navigate = useNavigate(); + const form = useAppForm({ + defaultValues: { + name: "", + email: "", + password: "", + confirmPassword: "", + }, + onSubmit: async ({ value }) => { + if (value.password !== value.confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + const { data, error } = await authClient.signUp.email({ + name: value.name, + email: value.email, + password: value.password, + callbackURL: `${window.location.origin}/lst/app`, + }); + + if (data) { + toast.success(`Welcome ${value.name}, to lst.`); + navigate({ to: "/" }); + } + + if (error) { + toast.error(error.message); + } + }, + }); + return ( +
+ + + Create an account + Fill in your details to get started + + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + {/* Username */} + + {(field) => ( + + )} + + + {/* Email */} + + {(field) => ( + + )} + + + {/* Password */} + + {(field) => ( + + )} + + + {/* Confirm Password */} + + {(field) => ( + + )} + + +
+ + Sign Up + +
+
+ +
+ Already have an account?{" "} + + Log in + +
+
+
+
+ ); } diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 6e95d2b..ebea132 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,8 +1,8 @@ import { createFileRoute } from "@tanstack/react-router"; import z from "zod"; - -import { useSession } from "../lib/auth-client"; +import { Button } from "@/components/ui/button"; +import { authClient, useSession } from "../lib/auth-client"; export const Route = createFileRoute("/")({ validateSearch: z.object({ @@ -13,11 +13,12 @@ export const Route = createFileRoute("/")({ }); function Index() { - const { data: session, isPending } = useSession(); + const { isPending } = useSession(); if (isPending) return
Loading...
; // if (!session) return + return (

Welcome Home!

diff --git a/tsconfig.json b/tsconfig.json index cfe0069..a18c4ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ //"allowImportingTsExtensions": true, "noEmit": false }, - "include": ["backend/**/*", "types"], + "include": ["backend/**/*" +], "exclude": [ "node_modules", "frontend",