user forms added
This commit is contained in:
@@ -3,7 +3,7 @@ import type { Server as HttpServer } from "node:http";
|
|||||||
//import { fileURLToPath } from "node:url";
|
//import { fileURLToPath } from "node:url";
|
||||||
import { instrument } from "@socket.io/admin-ui";
|
import { instrument } from "@socket.io/admin-ui";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { auth } from "utils/auth.utils.js";
|
|
||||||
import { createLogger } from "../logger/logger.controller.js";
|
import { createLogger } from "../logger/logger.controller.js";
|
||||||
import { allowedOrigins } from "../utils/cors.utils.js";
|
import { allowedOrigins } from "../utils/cors.utils.js";
|
||||||
import { registerEmitter } from "./roomEmitter.socket.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 type { Session, User } from "better-auth"; // adjust if needed
|
||||||
import { protectedRooms } from "./roomDefinitions.socket.js";
|
import { protectedRooms } from "./roomDefinitions.socket.js";
|
||||||
|
import { auth } from "../utils/auth.utils.js";
|
||||||
|
|
||||||
// declare module "socket.io" {
|
// declare module "socket.io" {
|
||||||
// interface Socket {
|
// interface Socket {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const auth = betterAuth({
|
|||||||
minPasswordLength: 8, // optional config
|
minPasswordLength: 8, // optional config
|
||||||
resetPasswordTokenExpirySeconds: process.env.RESET_EXPIRY_SECONDS, // time in seconds
|
resetPasswordTokenExpirySeconds: process.env.RESET_EXPIRY_SECONDS, // time in seconds
|
||||||
sendResetPassword: async ({ user, token }) => {
|
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(
|
const expiryMinutes = Math.floor(
|
||||||
parseInt(process.env.RESET_EXPIRY_SECONDS ?? "3600", 10) / 60,
|
parseInt(process.env.RESET_EXPIRY_SECONDS ?? "3600", 10) / 60,
|
||||||
);
|
);
|
||||||
@@ -137,5 +137,3 @@ export const auth = betterAuth({
|
|||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type Session = typeof auth.$Infer.Session;
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
@@ -6,7 +5,6 @@ import type { Transporter } from "nodemailer";
|
|||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
import type Mail from "nodemailer/lib/mailer/index.js";
|
import type Mail from "nodemailer/lib/mailer/index.js";
|
||||||
import type { Address } 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 hbs from "nodemailer-express-handlebars";
|
||||||
import { returnFunc } from "./returnHelper.utils.js";
|
import { returnFunc } from "./returnHelper.utils.js";
|
||||||
import { tryCatch } from "./trycatch.utils.js";
|
import { tryCatch } from "./trycatch.utils.js";
|
||||||
@@ -24,62 +22,36 @@ interface EmailData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const sendEmail = async (data: EmailData) => {
|
export const sendEmail = async (data: EmailData) => {
|
||||||
let transporter: Transporter;
|
const fromEmail: string = `DoNotReply@mail.alpla.com`;
|
||||||
let fromEmail: string | Address;
|
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")) {
|
// creating the handlbar options
|
||||||
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
|
|
||||||
const viewPath = path.resolve(
|
const viewPath = path.resolve(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
"./mailViews",
|
"../utils/mailViews/",
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlebarOptions = {
|
const handlebarOptions = {
|
||||||
viewEngine: {
|
viewEngine: {
|
||||||
extname: ".hbs",
|
extname: ".hbs",
|
||||||
defaultLayout: "",
|
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
|
||||||
|
defaultLayout: "", // Specify the default layout
|
||||||
partialsDir: viewPath,
|
partialsDir: viewPath,
|
||||||
},
|
},
|
||||||
viewPath: viewPath,
|
viewPath: viewPath,
|
||||||
extName: ".hbs",
|
extName: ".hbs", // File extension for Handlebars templates
|
||||||
};
|
};
|
||||||
|
|
||||||
transporter.use("compile", hbs(handlebarOptions));
|
transporter.use("compile", hbs(handlebarOptions));
|
||||||
@@ -89,7 +61,7 @@ export const sendEmail = async (data: EmailData) => {
|
|||||||
to: data.email,
|
to: data.email,
|
||||||
subject: data.subject,
|
subject: data.subject,
|
||||||
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
|
//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')
|
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
|
||||||
context: data.context,
|
context: data.context,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function Header() {
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link to="/">Profile</Link>
|
<Link to="/user/profile">Profile</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* <DropdownMenuItem>Billing</DropdownMenuItem>
|
{/* <DropdownMenuItem>Billing</DropdownMenuItem>
|
||||||
|
|||||||
10
frontend/src/components/ui/spinner.tsx
Normal file
10
frontend/src/components/ui/spinner.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
|
return (
|
||||||
|
<Loader2Icon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Spinner }
|
||||||
@@ -20,7 +20,7 @@ export const InputField = ({ label, inputType, required }: InputFieldProps) => {
|
|||||||
const field = useFieldContext<any>();
|
const field = useFieldContext<any>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3 mt-2">
|
||||||
<Label htmlFor={field.name}>{label}</Label>
|
<Label htmlFor={field.name}>{label}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={field.name}
|
id={field.name}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useStore } from "@tanstack/react-form";
|
import { useStore } from "@tanstack/react-form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useFormContext } from ".";
|
import { useFormContext } from ".";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
||||||
type SubmitButtonProps = {
|
type SubmitButtonProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -17,7 +18,9 @@ export const SubmitButton = ({ children }: SubmitButtonProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
{children}
|
{isSubmitting ? <><Spinner data-icon="inline-start" /> Submitting </> : <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Route as AdminLogsRouteImport } from './routes/admin/logs'
|
|||||||
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||||
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
|
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
|
||||||
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
|
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
|
||||||
|
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
|
||||||
|
|
||||||
const AboutRoute = AboutRouteImport.update({
|
const AboutRoute = AboutRouteImport.update({
|
||||||
id: '/about',
|
id: '/about',
|
||||||
@@ -46,12 +47,18 @@ const authUserResetpasswordRoute = authUserResetpasswordRouteImport.update({
|
|||||||
path: '/user/resetpassword',
|
path: '/user/resetpassword',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const authUserProfileRoute = authUserProfileRouteImport.update({
|
||||||
|
id: '/(auth)/user/profile',
|
||||||
|
path: '/user/profile',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
|
'/user/profile': typeof authUserProfileRoute
|
||||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
'/user/signup': typeof authUserSignupRoute
|
'/user/signup': typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
@@ -60,6 +67,7 @@ export interface FileRoutesByTo {
|
|||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
|
'/user/profile': typeof authUserProfileRoute
|
||||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
'/user/signup': typeof authUserSignupRoute
|
'/user/signup': typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
@@ -69,6 +77,7 @@ export interface FileRoutesById {
|
|||||||
'/about': typeof AboutRoute
|
'/about': typeof AboutRoute
|
||||||
'/(auth)/login': typeof authLoginRoute
|
'/(auth)/login': typeof authLoginRoute
|
||||||
'/admin/logs': typeof AdminLogsRoute
|
'/admin/logs': typeof AdminLogsRoute
|
||||||
|
'/(auth)/user/profile': typeof authUserProfileRoute
|
||||||
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
'/(auth)/user/signup': typeof authUserSignupRoute
|
'/(auth)/user/signup': typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
@@ -79,6 +88,7 @@ export interface FileRouteTypes {
|
|||||||
| '/about'
|
| '/about'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/admin/logs'
|
| '/admin/logs'
|
||||||
|
| '/user/profile'
|
||||||
| '/user/resetpassword'
|
| '/user/resetpassword'
|
||||||
| '/user/signup'
|
| '/user/signup'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
@@ -87,6 +97,7 @@ export interface FileRouteTypes {
|
|||||||
| '/about'
|
| '/about'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/admin/logs'
|
| '/admin/logs'
|
||||||
|
| '/user/profile'
|
||||||
| '/user/resetpassword'
|
| '/user/resetpassword'
|
||||||
| '/user/signup'
|
| '/user/signup'
|
||||||
id:
|
id:
|
||||||
@@ -95,6 +106,7 @@ export interface FileRouteTypes {
|
|||||||
| '/about'
|
| '/about'
|
||||||
| '/(auth)/login'
|
| '/(auth)/login'
|
||||||
| '/admin/logs'
|
| '/admin/logs'
|
||||||
|
| '/(auth)/user/profile'
|
||||||
| '/(auth)/user/resetpassword'
|
| '/(auth)/user/resetpassword'
|
||||||
| '/(auth)/user/signup'
|
| '/(auth)/user/signup'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
@@ -104,6 +116,7 @@ export interface RootRouteChildren {
|
|||||||
AboutRoute: typeof AboutRoute
|
AboutRoute: typeof AboutRoute
|
||||||
authLoginRoute: typeof authLoginRoute
|
authLoginRoute: typeof authLoginRoute
|
||||||
AdminLogsRoute: typeof AdminLogsRoute
|
AdminLogsRoute: typeof AdminLogsRoute
|
||||||
|
authUserProfileRoute: typeof authUserProfileRoute
|
||||||
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
||||||
authUserSignupRoute: typeof authUserSignupRoute
|
authUserSignupRoute: typeof authUserSignupRoute
|
||||||
}
|
}
|
||||||
@@ -152,6 +165,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof authUserResetpasswordRouteImport
|
preLoaderRoute: typeof authUserResetpasswordRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
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,
|
AboutRoute: AboutRoute,
|
||||||
authLoginRoute: authLoginRoute,
|
authLoginRoute: authLoginRoute,
|
||||||
AdminLogsRoute: AdminLogsRoute,
|
AdminLogsRoute: AdminLogsRoute,
|
||||||
|
authUserProfileRoute: authUserProfileRoute,
|
||||||
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
||||||
authUserSignupRoute: authUserSignupRoute,
|
authUserSignupRoute: authUserSignupRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
90
frontend/src/routes/(auth)/-components/ChangePassword.tsx
Normal file
90
frontend/src/routes/(auth)/-components/ChangePassword.tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<Card className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Change password</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.AppField name="currentPassword">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="Enter your current password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<form.AppField name="newPassword">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="New password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<form.AppField name="confirmPassword">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="Re-enter your password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Update Profile</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
|||||||
76
frontend/src/routes/(auth)/-components/ResetForm.tsx
Normal file
76
frontend/src/routes/(auth)/-components/ResetForm.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<Card className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Reset your password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email address and we’ll send you a reset link
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.AppField name="email">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField label="Email" inputType="email" required />
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Send Reset Link</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
Remembered your password?{" "}
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
||||||
|
>
|
||||||
|
Back to login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
frontend/src/routes/(auth)/-components/ResetPassword.tsx
Normal file
94
frontend/src/routes/(auth)/-components/ResetPassword.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<Card className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Reset your password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email address and we’ll send you a reset link
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.AppField name="password">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="New Password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
<form.AppField name="confirmPassword">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputPasswordField
|
||||||
|
label="Confirm Password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Send Reset Link</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
Remembered your password?{" "}
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
||||||
|
>
|
||||||
|
Back to login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
frontend/src/routes/(auth)/user.profile.tsx
Normal file
99
frontend/src/routes/(auth)/user.profile.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex justify-center mt-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Card className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Profile</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Change your profile and password below
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.AppField name="name">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Name"
|
||||||
|
inputType="string"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Update Profile</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ChangePassword />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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')({
|
export const Route = createFileRoute("/(auth)/user/resetpassword")({
|
||||||
component: RouteComponent,
|
validateSearch: z.object({
|
||||||
})
|
token: z.string().optional(),
|
||||||
|
}),
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Hello "/(auth)/user/resetpassword"!</div>
|
const { token } = Route.useSearch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
{token ? <ResetPassword token={token} /> : <ResetForm />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')({
|
export const Route = createFileRoute("/(auth)/user/signup")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Hello "/(auth)/user/signup"!</div>
|
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 (
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<Card className="p-6 w-96">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create an account</CardTitle>
|
||||||
|
<CardDescription>Fill in your details to get started</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Username */}
|
||||||
|
<form.AppField name="name">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Name"
|
||||||
|
inputType="text"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<form.AppField name="email">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Email address"
|
||||||
|
inputType="email"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<form.AppField name="password">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Password"
|
||||||
|
inputType="password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
{/* Confirm Password */}
|
||||||
|
<form.AppField name="confirmPassword">
|
||||||
|
{(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Confirm Password"
|
||||||
|
inputType="password"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Sign Up</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to={"/login"}
|
||||||
|
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useSession } from "../lib/auth-client";
|
import { authClient, useSession } from "../lib/auth-client";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: z.object({
|
validateSearch: z.object({
|
||||||
@@ -13,11 +13,12 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const { data: session, isPending } = useSession();
|
const { isPending } = useSession();
|
||||||
|
|
||||||
if (isPending)
|
if (isPending)
|
||||||
return <div className="flex justify-center mt-10">Loading...</div>;
|
return <div className="flex justify-center mt-10">Loading...</div>;
|
||||||
// if (!session) return <button>Sign In</button>
|
// if (!session) return <button>Sign In</button>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center mt-10">
|
<div className="flex justify-center mt-10">
|
||||||
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
|
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
//"allowImportingTsExtensions": true,
|
//"allowImportingTsExtensions": true,
|
||||||
"noEmit": false
|
"noEmit": false
|
||||||
},
|
},
|
||||||
"include": ["backend/**/*", "types"],
|
"include": ["backend/**/*"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"frontend",
|
"frontend",
|
||||||
|
|||||||
Reference in New Issue
Block a user