middleware('guest', ['except' => ['getVerification', 'getVerificationError']]); } /** * @param array $data */ protected function create(array $data): User { $user = User::create([ 'username' => $data['username'], 'email' => $data['email'], 'password' => User::hashPassword($data['password']), 'host' => $data['host'] ?? '', 'roles_id' => $data['roles_id'], 'notes' => $data['notes'], 'invites' => $data['defaultinvites'], 'api_token' => md5(Str::random(40)), ]); $role = Role::query()->where('id', '=', $data['roles_id'])->first(); if ($role !== null) { $user->syncRoles([$role->name]); PermissionSyncHelper::grantInheritedPermissions($user); } return $user; } /** * @return RedirectResponse|Redirector|void * * @throws ValidationException */ public function register(RegisterRegisterRequest $request) { $error = ''; $inviteCode = ''; $showRegister = 1; if ($request->has('invitecode')) { $inviteCode = $request->input('invitecode'); $this->inviteCodeQuery = '&invitecode='.$inviteCode; } // Handle invitation token from URL (for email links) if ($request->has('token')) { $inviteCode = $request->input('token'); $this->inviteCodeQuery = '&token='.$inviteCode; } $validator = Validator::make($request->all(), [ 'username' => ['required', 'string', 'min:5', 'max:255', 'unique:users'], 'email' => [ 'required', 'string', 'email', 'max:255', Rule::unique('users', 'email')->where(fn ($query) => $query->whereNull('deleted_at')), new ValidEmailDomain, ], 'password' => ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised()], ], [ 'password.min' => 'The password must be at least 8 characters.', 'password.letters' => 'The password must contain letters.', 'password.mixed_case' => 'The password must contain both uppercase and lowercase letters.', 'password.numbers' => 'The password must contain at least one number.', 'password.symbols' => 'The password must contain at least one symbol.', 'password.uncompromised' => 'The password appears in a data breach and should not be used.', ]); if ($validator->fails()) { return redirect()->back() ->withErrors($validator) ->withInput($request->except('password', 'password_confirmation')); } $action = $request->input('action') ?? 'view'; switch ($action) { case 'submit': $userName = (string) $request->input('username'); $password = (string) $request->input('password'); $email = (string) $request->input('email'); // Check if the email is associated with a soft-deleted user $existingUser = User::withTrashed()->where('email', $email)->first(); if ($existingUser && $existingUser->trashed()) { return $this->redirectBackWithRegistrationError( $request, 'This email address belongs to a deactivated account. Please contact us if you need help reactivating it.' ); } // Get the default user role. $userDefault = Role::query()->where('isdefault', '=', 1)->first(); // Check invitation validity using custom system $invitationValid = $this->isInvitationValid($inviteCode, $email); $registrationOpen = Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_OPEN; if ($invitationValid || $registrationOpen) { // Get invited_by from invitation if available $invitedBy = 0; $invitation = null; if (! empty($inviteCode)) { $invitation = Invitation::findValidByToken($inviteCode); if ($invitation) { $invitedBy = $invitation->invited_by; // Validate email matches invitation if (! empty($invitation->email) && ! $this->emailsMatch($invitation->email, $email)) { return $this->redirectBackWithRegistrationError( $request, 'The email address you entered does not match the invitation. Please use the invited email address to register.' ); } } } $userData = [ 'username' => $userName, 'password' => $password, 'email' => $email, 'host' => $request->ip(), 'roles_id' => $userDefault !== null ? $userDefault['id'] : User::ROLE_USER, 'notes' => '', 'defaultinvites' => $userDefault !== null ? $userDefault['defaultinvites'] : Invitation::DEFAULT_INVITES, ]; // Apply invitation metadata if available if ($invitation && $invitation->metadata) { if (isset($invitation->metadata['role'])) { $userData['roles_id'] = $invitation->metadata['role']; } } try { DB::transaction(function () use ($invitation, $userData): void { $user = $this->create($userData); if ($invitation && ! $invitation->markAsUsed($user->id)) { throw new \RuntimeException('Failed to mark invitation as used.'); } }); } catch (Throwable $exception) { Log::error('User registration failed', [ 'username' => $userName, 'email' => $email, 'ip' => $request->ip(), 'error' => $exception->getMessage(), ]); return $this->redirectBackWithRegistrationError( $request, 'We could not complete your registration right now. Please try again in a few minutes. If the problem continues, contact support.' ); } return redirect() ->route('login') ->with('message', 'Your account has been created. You will receive an account confirmation email shortly. Please verify your email address before logging in.') ->with('message_type', 'info'); } $error = 'Invalid or expired invitation token!'; break; case 'view': // Don't set showRegister here - let showRegistrationForm handle it // Only validate invite code if present if (($inviteCode !== null) && ! $this->isInvitationTokenValid($inviteCode)) { $error = 'Invalid invitation token!'; } break; } return $this->showRegistrationForm($request, $error, $showRegister); } public function showRegistrationForm(Request $request, string $error = '', int $showRegister = 0): mixed { $inviteCode = ''; if ($request->has('invitecode')) { $inviteCode = $request->input('invitecode'); $this->inviteCodeQuery = '&invitecode='.$inviteCode; } // Handle invitation token from URL (for email links) if ($request->has('token')) { $inviteCode = $request->input('token'); $this->inviteCodeQuery = '&token='.$inviteCode; } $emailFromInvite = ''; if ((int) Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_INVITE) { if (! empty($inviteCode)) { if ($this->isInvitationTokenValid($inviteCode)) { $error = ''; $showRegister = 1; // Pre-fill email if invitation has one $invitation = Invitation::findValidByToken($inviteCode); if ($invitation && ! empty($invitation->email)) { $emailFromInvite = $invitation->email; } } else { $error = 'Invalid or expired invitation token!'; $showRegister = 0; } } else { $error = 'Registrations are currently invite only.'; $showRegister = 0; } } elseif ((int) Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_CLOSED) { $error = 'Registrations are currently closed.'; $showRegister = 0; } elseif ($request->has('invitecode') || $request->has('token')) { $error = 'Registration is open, you don\'t need the invite code to register.'; $showRegister = 0; } else { $showRegister = 1; } return view('auth.register')->with([ 'showregister' => $showRegister, 'error' => $error, 'email' => $emailFromInvite, 'invite_code_query' => $this->inviteCodeQuery, 'invitecode' => $inviteCode, ]); } /** * Check if invitation is valid for given email */ private function isInvitationValid(string $token, string $email): bool { if (empty($token)) { return false; } $invitation = Invitation::findValidByToken($token); if (! $invitation) { return false; } // If invitation has specific email, validate it matches if (! empty($invitation->email) && ! $this->emailsMatch($invitation->email, $email)) { return false; } return true; } /** * Check if invitation token is valid (without email check) */ private function isInvitationTokenValid(string $token): bool { if (empty($token)) { return false; } $invitation = Invitation::findValidByToken($token); return $invitation !== null; } private function redirectBackWithRegistrationError(Request $request, string $message): RedirectResponse { return redirect() ->back() ->withErrors(['registration' => $message]) ->withInput($request->except('password', 'password_confirmation')); } private function emailsMatch(string $firstEmail, string $secondEmail): bool { return strcasecmp(trim($firstEmail), trim($secondEmail)) === 0; } }