settings->get(Setting::ClientsAutoApprove) === true; } /** * Whether an address is free for a new account. * * The unique index on `email` spans soft-deleted rows — AvailableEmailRule * is built on exactly that, so a deleted account keeps its address until * erasure takes the row away. The registration form learns this from * validation. The machine paths have no form to validate: a directory or * an identity provider hands over an address and provision() inserts it, * so without asking first the insert raises a QueryException in the * middle of somebody's sign-in. */ public function addressIsFree(string $email): bool { return ! User::withTrashed()->where('email', $email)->exists(); } /** * @param bool|null $autoApprove Null asks Setting::ClientsAutoApprove, * which is the right question for the * public registration form. A caller * that has already established who * somebody is — LDAP, an identity * provider — passes its own answer * instead. * @param array $context Placeholders for the action's * log template, e.g. which * provider an account came from. */ public function provision( string $name, string $email, string $password, Action $action, AuthSource $source = AuthSource::Local, ?string $ldapDn = null, ?bool $autoApprove = null, array $context = [], ): User { $autoApprove ??= $this->autoApproves(); // Only when the account arrives already approved. A request that // still needs a decision is not yet a client this installation has // taken on, and counting one would let a stranger exhaust a paid // limit from the registration form — see SeatAllowance. The guard // for those sits on approval instead. if ($autoApprove) { $this->seats->guardClient(); } $client = User::create([ 'type' => UserType::Client, 'active' => $autoApprove, 'account_requested' => ! $autoApprove, 'role_id' => Role::query()->where('name', SystemRole::Client->value)->value('id'), 'name' => $name, 'email' => $email, 'password' => $password, ]); // Not mass-assignable: where an account's credentials live is a // security decision, not an attribute a form may set. if ($source !== AuthSource::Local || $ldapDn !== null) { $client->forceFill([ 'auth_source' => $source, 'ldap_dn' => $ldapDn, 'ldap_synced_at' => $ldapDn === null ? null : now(), ])->save(); } $this->activity->log($action, $client, $client, $context); $this->joinAutoGroup($client); $this->notifyAdministrators($client, pending: ! $autoApprove); return $client; } /** * The group every self-provisioned client joins, if one is configured. * Direct membership, no approval — an administrator chose this in * settings. */ private function joinAutoGroup(User $client): void { $autoGroupId = (int) $this->settings->get(Setting::ClientsAutoGroup); if ($autoGroupId <= 0) { return; } $group = Group::query()->find($autoGroupId); $group?->members()->syncWithoutDetaching([$client->id]); } private function notifyAdministrators(User $client, bool $pending): void { if ($this->settings->get(Setting::EmailNotificationsEnabled) !== true) { return; } $addresses = $this->settings->get(Setting::AdminNotificationEmails); foreach (is_array($addresses) ? $addresses : [] as $address) { Notification::route('mail', $address)->notify( new AdminClientRegisteredNotification($client->name, $client->email, $pending) ); } } }