config = model(Configuration::class); $this->user = model(User::class); $this->userRole = model(UserRole::class); $this->role = model(Role::class); $this->parent = model(ParentModel::class); $this->schoolIdService = app(SchoolIdService::class); $this->emailService = app(EmailService::class); $this->phoneFormatter = app(PhoneFormatterService::class); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); } public function index(): JsonResponse { $session = session(); log_message('debug', 'CAPTCHA session value: ' . ($session->get('captcha_answer') ?? '')); if (!$session->has('captcha_answer')) { $length = random_int(4, 8); $session->put('captcha_answer', $this->generateCaptchaText($length)); } return $this->respondSuccess([ 'captcha_question' => $session->get('captcha_answer'), ], 'Captcha generated'); } public function registrationSuccess(): JsonResponse { $session = session(); if (!$session->has('user_email')) { return $this->respondError('No registration session found.', Response::HTTP_BAD_REQUEST); } return $this->respondSuccess([ 'email' => $session->get('user_email'), ], 'Registration success data'); } public function register(): JsonResponse { $payload = $this->payloadData(); log_message('debug', 'CAPTCHA session value: ' . (session()->get('captcha_answer') ?? '')); $formatted = $this->formatUserInput($payload); $validationPayload = array_merge($payload, $formatted); $captcha = (string) ($payload['captcha'] ?? ''); $isParent = !empty($payload['is_parent']) && (string) $payload['is_parent'] === '1'; $noSecondParentInfo = !empty($payload['no_second_parent_info']) && (string) $payload['no_second_parent_info'] === '1'; $rules = [ 'firstname' => ['required', 'regex:/^[a-zA-Z\s-]+$/', 'min:2', 'max:30'], 'lastname' => ['required', 'regex:/^[a-zA-Z\s-]+$/', 'min:2', 'max:30'], 'gender' => ['required', Rule::in(['Male', 'Female'])], 'email' => ['required', 'email', 'max:50'], 'confirm_email' => ['required', 'same:email'], 'cellphone' => ['required', 'regex:/^[\d\s\-\(\)\.]+$/', 'min:10', 'max:20'], 'address_street' => ['nullable', 'regex:/^[a-zA-Z0-9\s\-]+$/', 'max:150'], 'apt' => ['nullable', 'regex:/^[a-zA-Z0-9\s-]+$/', 'max:10'], 'city' => ['required', 'regex:/^[a-zA-Z\s]+$/', 'min:2', 'max:30'], 'state' => ['required', Rule::in(['CT', 'ME', 'MA', 'NH', 'NY', 'RI', 'VT'])], 'zip' => ['required', 'regex:/^\d{5}$/'], 'accept_school_policy' => ['required', 'accepted'], 'captcha' => ['required', 'alpha_num', 'min:4', 'max:10'], ]; if ($isParent && !$noSecondParentInfo) { $rules = array_merge($rules, [ 'second_firstname' => ['required', 'regex:/^[a-zA-Z\s-]+$/', 'min:2', 'max:30'], 'second_lastname' => ['required', 'regex:/^[a-zA-Z\s-]+$/', 'min:2', 'max:30'], 'second_gender' => ['required', Rule::in(['Male', 'Female'])], 'second_email' => ['required', 'email', 'max:50'], 'second_cellphone' => ['required', 'regex:/^[\d\s\-\(\)\.]+$/', 'min:10', 'max:20'], ]); } $errors = $this->validateRequest($validationPayload, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } if ($isParent && !$noSecondParentInfo && !$this->hasSecondParentData($formatted)) { return $this->respondValidationError([ 'second_parent_info' => ['As a parent, please provide second parent information or check "No second parent info".'], ]); } if ($captcha !== (string) session()->get('captcha_answer')) { return $this->respondValidationError([ 'captcha' => ['Incorrect CAPTCHA answer. Please try again.'], ], 'Incorrect CAPTCHA answer.'); } $email = $formatted['email'] ?? ''; $existingUser = $this->user->newQuery()->where('email', $email)->first(); if ($existingUser) { $isVerified = (int) ($existingUser->is_verified ?? 0); $token = $existingUser->token ?? null; if (!empty($token) && $isVerified === 0) { return $this->respondError( 'This email address is already registered and is pending activation. Please check your email to activate your account.', Response::HTTP_CONFLICT ); } return $this->respondError( 'The email address you entered is already in use. Please try a different one.', Response::HTTP_CONFLICT ); } $roleName = $isParent ? 'parent' : 'guest'; $role = $this->role->newQuery()->where('name', $roleName)->first(); if (!$role) { return $this->respondError('Role not found', Response::HTTP_BAD_REQUEST); } $token = bin2hex(random_bytes(48)); $userData = [ 'firstname' => $formatted['firstname'] ?? null, 'lastname' => $formatted['lastname'] ?? null, 'gender' => $formatted['gender'] ?? null, 'email' => $email, 'cellphone' => $formatted['cellphone'] ?? null, 'address_street' => $formatted['address_street'] ?? null, 'apt' => $formatted['apt'] ?? null, 'city' => $formatted['city'] ?? null, 'state' => $formatted['state'] ?? null, 'zip' => $formatted['zip'] ?? null, 'token' => $token, 'is_verified' => 0, 'accept_school_policy' => (int) ($formatted['accept_school_policy'] ?? 0), 'status' => 'Inactive', 'school_id' => $this->schoolIdService->generate(), 'semester' => $this->semester, 'school_year' => $this->schoolYear, ]; DB::beginTransaction(); try { $user = $this->user->newQuery()->create($userData); $firstParentId = $user->id ?? null; if (!$firstParentId) { DB::rollBack(); return $this->respondError('Registration failed, please try again.', Response::HTTP_INTERNAL_SERVER_ERROR); } $this->userRole->newQuery()->create([ 'user_id' => $firstParentId, 'role_id' => $role->id, 'semester' => $this->semester, 'school_year'=> $this->schoolYear, 'created_at' => utc_now(), ]); if ($isParent && !$noSecondParentInfo && $this->hasSecondParentData($formatted)) { $this->parent->newQuery()->create([ 'secondparent_firstname' => $formatted['second_firstname'] ?? null, 'secondparent_lastname' => $formatted['second_lastname'] ?? null, 'secondparent_gender' => $formatted['second_gender'] ?? null, 'secondparent_email' => $formatted['second_email'] ?? null, 'secondparent_phone' => $formatted['second_cellphone'] ?? null, 'firstparent_id' => $firstParentId, 'semester' => $this->semester, 'school_year' => $this->schoolYear, ]); } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); log_message('error', 'Registration failed: ' . $e->getMessage()); return $this->respondError('Registration failed, please try again.', Response::HTTP_INTERNAL_SERVER_ERROR); } $recipientName = trim(($formatted['firstname'] ?? '') . ' ' . ($formatted['lastname'] ?? '')); $emailData = [ 'recipientName' => $recipientName, 'activationLink'=> url('/user/confirm/' . $token), 'orgName' => 'Al Rahma Sunday School', 'contactInfo' => 'alrahma.isgl@gmail.com', 'logoUrl' => 'https://alrahmaisgl.org/assets/images/alrahma_logo.png', ]; $html = $this->buildEmailBody($emailData, $isParent); $this->emailService->send($email, 'Email Confirmation', $html, 'general'); session()->put('user_email', $email); session()->forget('captcha_answer'); return $this->respondSuccess([ 'email' => $email, ], 'Registration successful'); } private function generateCaptchaText(int $length): string { $characters = 'ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz0123456789'; $captchaText = ''; for ($i = 0; $i < $length; $i++) { $captchaText .= $characters[random_int(0, strlen($characters) - 1)]; } return $captchaText; } private function formatUserInput(array $post): array { $g = static function (string $key) use ($post): string { return trim((string) ($post[$key] ?? '')); }; $data = [ 'firstname' => $this->formatName($g('firstname')), 'lastname' => $this->formatName($g('lastname')), 'gender' => $g('gender'), 'email' => $this->formatEmail($g('email')), 'confirm_email' => $this->formatEmail($g('confirm_email')), 'cellphone' => $this->phoneFormatter->formatPhoneNumber($g('cellphone')), 'address_street' => $this->nullIfEmpty($this->formatAddress($g('address_street'))), 'apt' => $this->nullIfEmpty(strtoupper($g('apt'))), 'city' => $this->formatName($g('city')), 'state' => strtoupper($g('state')), 'zip' => $g('zip'), 'accept_school_policy' => (int) ($post['accept_school_policy'] ?? 0), ]; $noSecondInfo = !empty($post['no_second_parent_info']); $second_firstname = $this->formatName($g('second_firstname')); $second_lastname = $this->formatName($g('second_lastname')); $second_gender = $g('second_gender'); $second_email = $this->formatEmail($g('second_email')); $second_cellphone = $this->phoneFormatter->formatPhoneNumber($g('second_cellphone')); $secondProvided = !$noSecondInfo && ( $second_firstname !== '' || $second_lastname !== '' || $second_gender !== '' || $second_email !== '' || preg_replace('/\D/', '', $second_cellphone) !== '' ); if ($secondProvided) { $data['second_firstname'] = $second_firstname; $data['second_lastname'] = $second_lastname; $data['second_gender'] = $second_gender; $data['second_email'] = $second_email; $data['second_cellphone'] = $second_cellphone; } return $data; } private function formatName(string $name): string { $name = trim($name); return ucwords(strtolower($name), " -"); } private function formatEmail(?string $email): string { return strtolower(trim((string) $email)); } private function formatAddress(?string $address): string { $address = trim((string) $address); if ($address === '') { return ''; } return ucwords(strtolower($address)); } private function nullIfEmpty(?string $value): ?string { if ($value === null) { return null; } $value = trim((string) $value); return $value === '' ? null : $value; } private function hasSecondParentData(array $data): bool { return !empty($data['second_firstname']) || !empty($data['second_lastname']) || !empty($data['second_gender']) || !empty($data['second_email']) || (!empty($data['second_cellphone']) && preg_replace('/\D/', '', $data['second_cellphone']) !== ''); } private function buildEmailBody(array $data, bool $isParent): string { $audience = $isParent ? 'Parent' : 'Staff Member'; return <<
Dear {$data['recipientName']},
Thank you for registering with {$data['orgName']} as a {$audience}. Please confirm your email address by clicking the link below:
If the link above does not work, copy and paste this URL into your browser:
{$data['activationLink']}
For questions, reach us at {$data['contactInfo']}.
Thank you,
{$data['orgName']}