feat(auth): signupm, forgot passowrd, reset password all added

This commit is contained in:
2025-09-25 15:42:35 -05:00
parent a30fa8c9f4
commit 86dea6083e
34 changed files with 3698 additions and 49 deletions

View File

@@ -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() {
</>
</SidebarMenuItem>
))}
{userAccess(null, ["systemAdmin"]) && (
<TanStackRouterDevtools position="bottom-right" />
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>

View File

@@ -21,16 +21,17 @@ export default function Nav() {
<div className="m-1">
<ModeToggle />
</div>
{/* <div className="mr-1 ml-1">
{settings.length > 0 && (
<div className="m-1">
<Button>
<a
href={`https://${server[0].value}.alpla.net/lst/d`}
href={`${window.location.origin}/lst/d`}
target="_blank"
>
LST - Docs |
LST - Docs
</a>
)}
</div> */}
</Button>
</div>
{session ? (
<div className="m-1">
<DropdownMenu>

View File

@@ -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() {
<div className="flex min-h-screen">
<Sidebar collapsible="icon">
<Header />
{userAccess(null, ["systemAdmin", "admin"]) && <Admin />}
<SidebarContent>
{userAccess(null, ["systemAdmin", "admin"]) && <Admin />}
</SidebarContent>
<SidebarFooter>
<SidebarTrigger />

View File

@@ -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<any>();
const [show, setShow] = useState(false);
return (
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<div className="relative">
<Input
id={field.name}
type={show ? "text" : "password"}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
required={required}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShow((p) => !p)}
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
tabIndex={-1} // prevent messing with tab order
>
{show ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 (
<div className="ml-[25%]">
<div className="">
<LstCard className="p-3 w-96">
<CardHeader>
<CardTitle>Login to your account</CardTitle>
@@ -87,9 +89,8 @@ export default function LoginForm() {
<form.AppField
name="password"
children={(field) => (
<field.InputField
<field.InputPasswordField
label="Password"
inputType="password"
required={true}
/>
)}
@@ -102,7 +103,7 @@ export default function LoginForm() {
)}
/>
<Link
to="/"
to="/user/resetpassword"
className="inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
@@ -118,9 +119,12 @@ export default function LoginForm() {
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
<Link
to={"/user/signup"}
className="underline underline-offset-4"
>
Sign up
</a>
</Link>
</div>
</CardContent>
</LstCard>

View File

@@ -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 (
<div className="">
<LstCard className="p-6 w-96">
<CardHeader>
<CardTitle>Reset your password</CardTitle>
<CardDescription>
Enter your email address and well send you a reset link
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="email"
children={(field) => (
<field.InputField
label="Email address"
inputType="email"
required={true}
/>
)}
/>
<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>
</LstCard>
</div>
);
}

View File

@@ -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 (
<div className="">
<LstCard className="p-6 w-96">
<CardHeader>
<CardTitle>Set a new password</CardTitle>
<CardDescription>
Enter your new password below and confirm it to continue
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="password"
children={(field) => (
<field.InputPasswordField
label="New Password"
required={true}
/>
)}
/>
<form.AppField
name="confirmPassword"
// validators={{
// onChangeListenTo: ["password"],
// onChange: ({ value, fieldApi }) => {
// if (
// value !==
// fieldApi.form.getFieldValue("password")
// ) {
// return "Passwords do not match";
// }
// return undefined;
// },
// }}
children={(field) => (
<field.InputPasswordField
label="Confirm Password"
required={true}
/>
)}
/>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>
Reset Password
</form.SubmitButton>
</form.AppForm>
</div>
</form>
<div className="mt-6 text-center text-sm text-gray-600">
Remembered your account?{" "}
<Link
to="/login"
className="text-primary underline underline-offset-4 hover:text-primary/80"
>
Back to login
</Link>
</div>
</CardContent>
</LstCard>
</div>
);
}

View File

@@ -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 (
<div className="">
<LstCard 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="username"
children={(field) => (
<field.InputField
label="Username"
inputType="text"
required={true}
/>
)}
/>
{/* Email */}
<form.AppField
name="email"
children={(field) => (
<field.InputField
label="Email address"
inputType="email"
required={true}
/>
)}
/>
{/* Password */}
<form.AppField
name="password"
children={(field) => (
<field.InputField
label="Password"
inputType="password"
required={true}
/>
)}
/>
{/* Confirm Password */}
<form.AppField
name="confirmPassword"
children={(field) => (
<field.InputField
label="Confirm Password"
inputType="password"
required={true}
/>
)}
/>
<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>
</LstCard>
</div>
);
}

View File

@@ -23,7 +23,7 @@ export const Route = createFileRoute("/(auth)/login")({
function RouteComponent() {
return (
<div className="m-[25%]">
<div className="ml-[25%] mt-[0.5%]">
<LoginForm />
</div>
);

View File

@@ -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 (
<div className="ml-[25%] mt-[0.5%]">
{token ? (
<ResetPasswordForm token={token} />
) : (
<RequestResetPassword />
)}
</div>
);
}

View File

@@ -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 (
<div className="ml-[25%] mt-[0.5%]">
<SignupForm />
</div>
);
}

View File

@@ -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 (
<div>
@@ -26,16 +25,15 @@ const RootLayout = () => {
<ThemeProvider>
<div className="flex flex-col h-screen overflow-hidden">
<Nav />
<div className="flex flex-1 overflow-hidden">
<div className="flex flex-1 overflow-hidden">
<SidebarProvider defaultOpen={defaultOpen}>
<SideBarNav />
<div className="flex-1 overflow-y-auto">
<div className="flex-2 overflow-y-auto">
<Outlet />
</div>
</SidebarProvider>
</div>
<Toaster expand richColors closeButton />
<TanStackRouterDevtools />
</div>
</ThemeProvider>
</SessionGuard>

View File

@@ -1,25 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "../components/ui/button";
import { useAuth } from "../lib/authClient";
export const Route = createFileRoute("/")({
component: Index,
});
function Index() {
const { session } = useAuth();
return (
<div>
<div className="h-screen flex flex-col items-center justify-center">
<div>Welcome, {session ? session.user.username : "Guest"}</div>
<div>
<Button className="h-96 w-96">
<a href="/lst/d" target="_blank" className="text-4xl">
LST-DOCS
</a>
</Button>
</div>
</div>
<div className="h-screen flex flex-col items-center justify-center"></div>
</div>
);
}