mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
signup update
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
|||||||
PositionType,
|
PositionType,
|
||||||
Position,
|
Position,
|
||||||
Project,
|
Project,
|
||||||
UnitSetting,
|
|
||||||
GlobalUnitConfiguration,
|
GlobalUnitConfiguration,
|
||||||
Unit,
|
Unit,
|
||||||
EmployeeSignature,
|
EmployeeSignature,
|
||||||
@@ -64,7 +63,6 @@ const iamEntities = [
|
|||||||
PositionType,
|
PositionType,
|
||||||
Position,
|
Position,
|
||||||
Project,
|
Project,
|
||||||
UnitSetting,
|
|
||||||
GlobalUnitConfiguration,
|
GlobalUnitConfiguration,
|
||||||
Unit,
|
Unit,
|
||||||
EmployeeSignature,
|
EmployeeSignature,
|
||||||
@@ -98,10 +96,8 @@ const iamMigrationsGlob = join(
|
|||||||
);
|
);
|
||||||
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
|
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
|
||||||
|
|
||||||
export default registerAs(
|
export default registerAs("database", (): TypeOrmModuleOptions => {
|
||||||
"database",
|
return {
|
||||||
(): TypeOrmModuleOptions => {
|
|
||||||
return {
|
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
host: process.env.DB_HOST ?? "localhost",
|
host: process.env.DB_HOST ?? "localhost",
|
||||||
port: parseInt(process.env.DB_PORT ?? "5433", 10),
|
port: parseInt(process.env.DB_PORT ?? "5433", 10),
|
||||||
@@ -124,5 +120,4 @@ export default registerAs(
|
|||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: process.env.NODE_ENV === "development",
|
logging: process.env.NODE_ENV === "development",
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -3,12 +3,21 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ArrowRight, UserPlus, Loader2 } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
UserPlus,
|
||||||
|
Loader2,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
import { userType } from "@/enums/userType";
|
import { userType } from "@/enums/userType";
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
import type { SignupPayload } from "@/types/auth";
|
import type { SignupPayload } from "@/types/auth";
|
||||||
import AuthLayout from "@/components/auth/AuthLayout";
|
import AuthLayout from "@/components/auth/AuthLayout";
|
||||||
import PhoneInput from "@/components/auth/PhoneInput";
|
import PhoneInput from "@/components/auth/PhoneInput";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -18,23 +27,47 @@ import {
|
|||||||
FieldGroup,
|
FieldGroup,
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
|
|
||||||
const userSchema = z.object({
|
const passwordRequirements = [
|
||||||
email: z.string().email("Invalid email address"),
|
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 },
|
||||||
countryCode: z.string().min(1, "Country code is required"),
|
{ label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
|
||||||
phone: z
|
{ label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) },
|
||||||
.string()
|
{ label: "One number", test: (v: string) => /\d/.test(v) },
|
||||||
.min(9, "Phone number is too short")
|
{
|
||||||
.max(9, "Phone number is too long"),
|
label: "One special character",
|
||||||
userType: z.string(),
|
test: (v: string) => /[^A-Za-z0-9]/.test(v),
|
||||||
firstName: z.object({
|
},
|
||||||
en: z.string().min(2, "Name is required"),
|
] as const;
|
||||||
am: z.string().nullable(),
|
|
||||||
}),
|
const userSchema = z
|
||||||
lastName: z.object({
|
.object({
|
||||||
en: z.string().min(2, "Name is required"),
|
email: z.string().email("Invalid email address"),
|
||||||
am: z.string().nullable(),
|
countryCode: z.string().min(1, "Country code is required"),
|
||||||
}),
|
phone: z
|
||||||
});
|
.string()
|
||||||
|
.min(9, "Phone number is too short")
|
||||||
|
.max(9, "Phone number is too long"),
|
||||||
|
userType: z.string(),
|
||||||
|
firstName: z.object({
|
||||||
|
en: z.string().min(2, "Name is required"),
|
||||||
|
am: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
lastName: z.object({
|
||||||
|
en: z.string().min(2, "Name is required"),
|
||||||
|
am: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, "Password must be at least 8 characters")
|
||||||
|
.regex(/[A-Z]/, "Password must include an uppercase letter")
|
||||||
|
.regex(/[a-z]/, "Password must include a lowercase letter")
|
||||||
|
.regex(/\d/, "Password must include a number")
|
||||||
|
.regex(/[^A-Za-z0-9]/, "Password must include a special character"),
|
||||||
|
confirmPassword: z.string().min(1, "Please confirm your password"),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: "Passwords do not match",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof userSchema>;
|
type FormData = z.infer<typeof userSchema>;
|
||||||
|
|
||||||
@@ -43,10 +76,13 @@ export default function SignupPage() {
|
|||||||
const { signup } = useAuth();
|
const { signup } = useAuth();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(userSchema),
|
resolver: zodResolver(userSchema),
|
||||||
@@ -57,6 +93,8 @@ export default function SignupPage() {
|
|||||||
userType: userType.individual,
|
userType: userType.individual,
|
||||||
firstName: { en: "", am: "" },
|
firstName: { en: "", am: "" },
|
||||||
lastName: { en: "", am: "" },
|
lastName: { en: "", am: "" },
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,6 +111,8 @@ export default function SignupPage() {
|
|||||||
phoneNumber: `${data.countryCode}${normalizedPhone}`,
|
phoneNumber: `${data.countryCode}${normalizedPhone}`,
|
||||||
userType: data.userType,
|
userType: data.userType,
|
||||||
name: { en: `${data.firstName.en} ${data.lastName.en}`, am: "" },
|
name: { en: `${data.firstName.en} ${data.lastName.en}`, am: "" },
|
||||||
|
password: data.password,
|
||||||
|
confirmPassword: data.confirmPassword,
|
||||||
};
|
};
|
||||||
const result = await signup(payload);
|
const result = await signup(payload);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -171,6 +211,81 @@ export default function SignupPage() {
|
|||||||
countryCodeError={errors.countryCode}
|
countryCodeError={errors.countryCode}
|
||||||
phoneError={errors.phone}
|
phoneError={errors.phone}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Field data-invalid={Boolean(errors.password)}>
|
||||||
|
<FieldLabel>Password</FieldLabel>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="Create a strong password"
|
||||||
|
disabled={loading}
|
||||||
|
aria-invalid={Boolean(errors.password)}
|
||||||
|
className="pr-10"
|
||||||
|
{...register("password")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FieldError errors={[errors.password]} />
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{passwordRequirements.map((req) => {
|
||||||
|
const met = req.test(watch("password") ?? "");
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={req.label}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 text-xs transition-colors",
|
||||||
|
met ? "text-emerald-600" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{met ? (
|
||||||
|
<Check className="size-3" />
|
||||||
|
) : (
|
||||||
|
<X className="size-3" />
|
||||||
|
)}
|
||||||
|
{req.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field data-invalid={Boolean(errors.confirmPassword)}>
|
||||||
|
<FieldLabel>Confirm Password</FieldLabel>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
|
placeholder="Re-enter your password"
|
||||||
|
disabled={loading}
|
||||||
|
aria-invalid={Boolean(errors.confirmPassword)}
|
||||||
|
className="pr-10"
|
||||||
|
{...register("confirmPassword")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<EyeOff className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FieldError errors={[errors.confirmPassword]} />
|
||||||
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
<Button type="submit" disabled={loading} size="lg" className="w-full">
|
<Button type="submit" disabled={loading} size="lg" className="w-full">
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface SignupPayload {
|
|||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
userType: string;
|
userType: string;
|
||||||
name: { en: string; am: string };
|
name: { en: string; am: string };
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SignupResponse {
|
export interface SignupResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user