. */ namespace App; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Http\Request; use Illuminate\Support\Str; use Carbon\Carbon; use Awobaz\Compoships\Compoships; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use stdClass; class Account extends Authenticatable { use HasFactory; use Compoships; protected $with = ['passwords', 'activationExpiration', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries']; protected $hidden = ['expire_time', 'confirmation_key', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries']; protected $appends = ['realm', 'confirmation_key_expires', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary']; protected $casts = [ 'activated' => 'boolean', ]; protected $fillable = ['username', 'domain', 'email']; public static $dtmfProtocols = ['sipinfo' => 'SIPInfo', 'rfc2833' => 'RFC2833', 'sipmessage' => 'SIP Message']; public static function boot() { parent::boot(); static::deleted(function (Account $account) { StatisticsMessage::where('from_username', $account->username) ->where('from_domain', $account->domain) ->delete(); StatisticsCall::where('from_username', $account->username) ->where('from_domain', $account->domain) ->delete(); }); static::created(function (Account $account) { $account->provision(); $account->refresh(); }); } protected static function booted() { static::addGlobalScope('domain', function (Builder $builder) { if (Auth::hasUser() && Auth::user()->superAdmin) { return; } $builder->where('domain', config('app.sip_domain')); }); } public function scopeSip($query, string $sip) { if (\str_contains($sip, '@')) { list($username, $domain) = explode('@', $sip); return $query->where('username', $username) ->where('domain', $domain); }; return $query->where('id', '<', 0); } public static function subByContactsList($query, int $contactsListId) { return $query->from('accounts') ->whereIn('id', function ($query) use ($contactsListId) { $query->select('contact_id') ->from('contacts_list_contact') ->where('contacts_list_id', $contactsListId); }); } /** * Relations */ public function actions() { return $this->hasMany('App\AccountAction')->whereIn('account_id', function ($query) { $query->select('id') ->from('accounts') ->whereNotNull('dtmf_protocol'); }); } public function activationExpiration() { return $this->hasOne(ActivationExpiration::class); } public function apiKey() { return $this->hasOne(ApiKey::class); } public function contacts() { return $this->belongsToMany(Account::class, 'contacts', 'account_id', 'contact_id'); } public function vcardsStorage() { return $this->hasMany(VcardStorage::class); } public function contactsLists() { return $this->belongsToMany(ContactsList::class, 'account_contacts_list', 'account_id', 'contacts_list_id'); } public function dictionaryEntries() { return $this->hasMany(AccountDictionaryEntry::class); } public function getDictionaryAttribute() { if ($this->dictionaryEntries->isEmpty()) return new stdClass; return $this->dictionaryEntries->keyBy('key')->map(function ($entry) { return $entry->value; }); } public function setDictionaryEntry(string $key, string $value): AccountDictionaryEntry { $this->dictionaryEntries()->where('key', $key)->delete(); $entry = new AccountDictionaryEntry; $entry->account_id = $this->id; $entry->key = $key; $entry->value = $value; $entry->save(); return $entry; } public function nonces() { return $this->hasMany(DigestNonce::class); } public function passwords() { return $this->hasMany(Password::class); } public function types() { return $this->belongsToMany(AccountType::class); } public function sipDomain() { return $this->hasOne(SipDomain::class, 'domain', 'domain'); } public function statisticsFromCalls() { return $this->hasMany(StatisticsCall::class, ['from_username', 'from_domain'], ['username', 'domain']); } public function statisticsToCalls() { return $this->hasMany(StatisticsCall::class, ['to_username', 'to_domain'], ['username', 'domain']); } public function statisticsFromMessages() { return $this->hasMany(StatisticsMessage::class, ['from_username', 'from_domain'], ['username', 'domain']); } public function statisticsToMessageDevices() { return $this->hasMany(StatisticsMessageDevice::class, ['to_username', 'to_domain'], ['username', 'domain']); } /** * Tokens and codes */ public function currentRecoveryCode() { return $this->hasOne(RecoveryCode::class)->whereNotNull('code')->latestOfMany(); } public function recoveryCodes() { return $this->hasMany(RecoveryCode::class)->latest(); } public function phoneChangeCode() { return $this->hasOne(PhoneChangeCode::class)->whereNotNull('code')->latestOfMany(); } public function phoneChangeCodes() { return $this->hasMany(PhoneChangeCode::class)->latest(); } public function emailChangeCode() { return $this->hasOne(EmailChangeCode::class)->whereNotNull('code')->latestOfMany(); } public function emailChangeCodes() { return $this->hasMany(EmailChangeCode::class)->latest(); } public function currentProvisioningToken() { return $this->hasOne(ProvisioningToken::class)->where('used', false)->latestOfMany(); } public function provisioningTokens() { return $this->hasMany(ProvisioningToken::class)->latest(); } public function accountCreationToken() { return $this->hasOne(AccountCreationToken::class); } public function authTokens() { return $this->hasMany(AuthToken::class); } /** * Attributes */ public function getRecoveryCodeAttribute(): ?string { if ($this->currentRecoveryCode) { return $this->currentRecoveryCode->code; } return null; } public function getProvisioningTokenAttribute(): ?string { if ($this->currentProvisioningToken) { return $this->currentProvisioningToken->token; } return null; } public function getProvisioningTokenExpireAtAttribute(): ?string { if ($this->currentProvisioningToken) { return $this->currentProvisioningToken->expire_at; } return null; } public function getIdentifierAttribute(): string { return $this->attributes['username'] . '@' . $this->attributes['domain']; } public function getFullIdentifierAttribute(): string { $displayName = $this->attributes['display_name'] ? '"' . $this->attributes['display_name'] . '" ' : ''; return $displayName . 'getIdentifierAttribute() . '>'; } public function getRealmAttribute() { return config('app.realm'); } public function getResolvedRealmAttribute() { return config('app.realm') ?? $this->domain; } public function getConfirmationKeyExpiresAttribute() { if ($this->activationExpiration) { return $this->activationExpiration->expires->format('Y-m-d H:i:s'); } return null; } public function getSha256PasswordAttribute() { return $this->passwords()->where('algorithm', 'SHA-256')->exists(); } public static function dtmfProtocolsRule() { return implode(',', array_keys(self::$dtmfProtocols)); } public function getResolvedDtmfProtocolAttribute() { return self::$dtmfProtocols[$this->attributes['dtmf_protocol']]; } public function getSuperAdminAttribute(): bool { return SipDomain::where('domain', $this->domain)->where('super', true)->exists() && $this->admin; } /** * Utils */ public function activationExpired(): bool { return ($this->activationExpiration && $this->activationExpiration->isExpired()); } public function generateApiKey(?Request $request = null): ApiKey { $this->apiKey()->delete(); $apiKey = new ApiKey; $apiKey->account_id = $this->id; $apiKey->last_used_at = Carbon::now(); $apiKey->key = Str::random(40); $apiKey->ip = $request ? $request->ip() : '127.0.0.1'; $apiKey->save(); return $apiKey; } public function generateAuthToken(): AuthToken { // Clean the expired and previous ones AuthToken::where( 'created_at', '<', Carbon::now()->subMinutes(AuthToken::$expirationMinutes) )->orWhere('account_id', $this->id) ->delete(); $authToken = new AuthToken; $authToken->account_id = $this->id; $authToken->token = Str::random(32); $authToken->save(); return $authToken; } public function recover(?string $code = null): string { $recoveryCode = new RecoveryCode; $recoveryCode->code = $code ?? generatePin(); $recoveryCode->account_id = $this->id; if (request()) { $recoveryCode->fillRequestInfo(request()); } $recoveryCode->save(); return $recoveryCode->code; } public function provision(?string $token = null): string { $provisioningToken = new ProvisioningToken; $provisioningToken->token = $token ?? Str::random(WebAuthenticateController::$emailCodeSize); $provisioningToken->account_id = $this->id; if (request()) { $provisioningToken->fillRequestInfo(request()); } $provisioningToken->save(); return $provisioningToken->token; } public function setRole(string $role) { if ($role == 'end_user') { $this->admin = false; } if ($role == 'admin') { $this->admin = true; } $this->save(); } public function hasTombstone(): bool { return AccountTombstone::where('username', $this->attributes['username']) ->where('domain', $this->attributes['domain']) ->exists(); } public function createTombstone(): bool { if (!$this->hasTombstone()) { $tombstone = new AccountTombstone(); $tombstone->username = $this->attributes['username']; $tombstone->domain = $this->attributes['domain']; $tombstone->save(); return true; } return false; } public function failedRecentRecovery(): bool { $oneHourAgo = Carbon::now()->subHour(); return !empty($this->recovery_code) && $this->updated_at->greaterThan($oneHourAgo); } public function updatePassword(string $newPassword, ?string $algorithm = null) { $algorithm = $algorithm ?? config('app.account_default_password_algorithm'); $this->passwords()->delete(); $password = new Password; $password->account_id = $this->id; $password->password = bchash($this->username, $this->resolvedRealm, $newPassword, $algorithm); $password->algorithm = $algorithm; $password->save(); } public function toVcard4() { $vcard = 'BEGIN:VCARD VERSION:4.0 KIND:individual IMPP:sip:' . $this->getIdentifierAttribute(); $vcard .= ' FN:'; $vcard .= !empty($this->display_name) ? $this->display_name : $this->getIdentifierAttribute(); if ($this->dtmf_protocol) { $vcard .= ' X-LINPHONE-ACCOUNT-DTMF-PROTOCOL:' . $this->dtmf_protocol; } foreach ($this->types as $type) { $vcard .= ' X-LINPHONE-ACCOUNT-TYPE:' . $type->key; } foreach ($this->actions as $action) { $vcard .= ' X-LINPHONE-ACCOUNT-ACTION:' . $action->key . ';' . $action->code; } return $vcard . ' END:VCARD'; } }