car_mms/app/routes/signup.tsx
2025-09-11 14:22:27 +03:00

413 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useLoaderData, useNavigation } from "@remix-run/react";
import { validateSignUp, createUser, isSignupAllowed } from "~/lib/auth-helpers.server";
import { createUserSession, getUserId } from "~/lib/auth.server";
import { AUTH_ERRORS } from "~/lib/auth-constants";
import type { SignUpFormData } from "~/types/auth";
export const meta: MetaFunction = () => {
return [
{ title: "إنشاء حساب - نظام إدارة صيانة السيارات" },
{ name: "description", content: "إنشاء حساب جديد في نظام إدارة صيانة السيارات" },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
// Import the redirect middleware
const { redirectIfAuthenticated } = await import("~/lib/auth-middleware.server");
await redirectIfAuthenticated(request);
const url = new URL(request.url);
const adminOverride = url.searchParams.get("admin_override") === "true";
// Check if signup is allowed (only when no admin users exist or admin override)
const signupAllowed = await isSignupAllowed();
if (!signupAllowed && !adminOverride) {
return redirect("/signin?error=signup_disabled");
}
return json({ signupAllowed: signupAllowed || adminOverride });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const adminOverride = formData.get("admin_override") === "true";
// Check if signup is still allowed
const signupAllowed = await isSignupAllowed();
if (!signupAllowed && !adminOverride) {
return json(
{
errors: [{ message: AUTH_ERRORS.SIGNUP_DISABLED }],
values: {}
},
{ status: 403 }
);
}
const name = formData.get("name");
const username = formData.get("username");
const email = formData.get("email");
const password = formData.get("password");
const confirmPassword = formData.get("confirmPassword");
// Validate form data types
if (
typeof name !== "string" ||
typeof username !== "string" ||
typeof email !== "string" ||
typeof password !== "string" ||
typeof confirmPassword !== "string"
) {
return json(
{
errors: [{ message: "بيانات النموذج غير صحيحة" }],
values: {
name: name || "",
username: username || "",
email: email || ""
}
},
{ status: 400 }
);
}
const signUpData: SignUpFormData = {
name: name.trim(),
username: username.trim(),
email: email.trim(),
password,
confirmPassword,
};
// Validate signup data
const validationResult = await validateSignUp(signUpData);
if (!validationResult.success) {
return json(
{
errors: validationResult.errors || [{ message: "فشل في التحقق من البيانات" }],
values: {
name: signUpData.name,
username: signUpData.username,
email: signUpData.email
}
},
{ status: 400 }
);
}
try {
// Create the user
const user = await createUser(signUpData);
// Create session and redirect to dashboard
return createUserSession(user.id, "/dashboard");
} catch (error) {
console.error("Error creating user:", error);
return json(
{
errors: [{ message: "حدث خطأ أثناء إنشاء الحساب" }],
values: {
name: signUpData.name,
username: signUpData.username,
email: signUpData.email
}
},
{ status: 500 }
);
}
}
export default function SignUp() {
const { signupAllowed } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
// Check if this is an admin override
const url = typeof window !== "undefined" ? new URL(window.location.href) : null;
const adminOverride = url?.searchParams.get("admin_override") === "true";
const getErrorMessage = (field?: string) => {
if (!actionData?.errors) return null;
const error = actionData.errors.find(e => e.field === field || (!e.field && !field));
return error?.message;
};
if (!signupAllowed) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8" dir="rtl">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-red-100">
<svg
className="h-6 w-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
التسجيل غير متاح
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
التسجيل غير متاح حالياً. يرجى الاتصال بالمسؤول.
</p>
<div className="mt-6">
<Link
to="/signin"
className="font-medium text-blue-600 hover:text-blue-500"
>
العودة إلى تسجيل الدخول
</Link>
</div>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8" dir="rtl">
<div className="max-w-md w-full space-y-8">
<div>
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-green-100">
<svg
className="h-6 w-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
إنشاء حساب جديد
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
أو{" "}
<Link
to="/signin"
className="font-medium text-blue-600 hover:text-blue-500"
>
تسجيل الدخول إلى حساب موجود
</Link>
</p>
</div>
<Form className="mt-8 space-y-6" method="post">
{adminOverride && (
<input type="hidden" name="admin_override" value="true" />
)}
{/* Display general errors */}
{getErrorMessage() && (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-red-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="mr-3">
<p className="text-sm text-red-800">{getErrorMessage()}</p>
</div>
</div>
</div>
)}
<div className="space-y-4">
{/* Name field */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
الاسم الكامل
</label>
<input
id="name"
name="name"
type="text"
autoComplete="name"
required
className={`mt-1 appearance-none relative block w-full px-3 py-2 border ${
getErrorMessage("name")
? "border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} rounded-md focus:z-10 sm:text-sm`}
placeholder="أدخل اسمك الكامل"
defaultValue={actionData?.values?.name}
/>
{getErrorMessage("name") && (
<p className="mt-1 text-sm text-red-600">{getErrorMessage("name")}</p>
)}
</div>
{/* Username field */}
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
اسم المستخدم
</label>
<input
id="username"
name="username"
type="text"
autoComplete="username"
required
className={`mt-1 appearance-none relative block w-full px-3 py-2 border ${
getErrorMessage("username")
? "border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} rounded-md focus:z-10 sm:text-sm`}
placeholder="أدخل اسم المستخدم"
defaultValue={actionData?.values?.username}
dir="ltr"
/>
{getErrorMessage("username") && (
<p className="mt-1 text-sm text-red-600">{getErrorMessage("username")}</p>
)}
</div>
{/* Email field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
البريد الإلكتروني
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className={`mt-1 appearance-none relative block w-full px-3 py-2 border ${
getErrorMessage("email")
? "border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} rounded-md focus:z-10 sm:text-sm`}
placeholder="أدخل بريدك الإلكتروني"
defaultValue={actionData?.values?.email}
dir="ltr"
/>
{getErrorMessage("email") && (
<p className="mt-1 text-sm text-red-600">{getErrorMessage("email")}</p>
)}
</div>
{/* Password field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
كلمة المرور
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
className={`mt-1 appearance-none relative block w-full px-3 py-2 border ${
getErrorMessage("password")
? "border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} rounded-md focus:z-10 sm:text-sm`}
placeholder="أدخل كلمة المرور (6 أحرف على الأقل)"
dir="ltr"
/>
{getErrorMessage("password") && (
<p className="mt-1 text-sm text-red-600">{getErrorMessage("password")}</p>
)}
</div>
{/* Confirm Password field */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
تأكيد كلمة المرور
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
required
className={`mt-1 appearance-none relative block w-full px-3 py-2 border ${
getErrorMessage("confirmPassword")
? "border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} rounded-md focus:z-10 sm:text-sm`}
placeholder="أعد إدخال كلمة المرور"
dir="ltr"
/>
{getErrorMessage("confirmPassword") && (
<p className="mt-1 text-sm text-red-600">{getErrorMessage("confirmPassword")}</p>
)}
</div>
</div>
<div>
<button
type="submit"
disabled={isSubmitting}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="absolute right-0 inset-y-0 flex items-center pr-3">
{isSubmitting ? (
<svg
className="animate-spin h-5 w-5 text-green-300"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<svg
className="h-5 w-5 text-green-500 group-hover:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
)}
</span>
{isSubmitting ? "جاري إنشاء الحساب..." : "إنشاء الحساب"}
</button>
</div>
</Form>
</div>
</div>
);
}