diff --git a/VERSION.txt b/VERSION.txt index f430587706..b5db1702f3 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.4.2 \ No newline at end of file +5.4.3 \ No newline at end of file diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 0fc9efb9da..3ee028fe98 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -163,7 +163,7 @@ class CompanySettings extends BaseSettings public $require_quote_signature = false; //@TODO ben to confirm //email settings - public $email_sending_method = 'default'; //enum 'default','gmail' //@implemented + public $email_sending_method = 'default'; //enum 'default','gmail','office365' //@implemented public $gmail_sending_user_id = '0'; //@implemented public $reply_to_email = ''; //@implemented diff --git a/app/Helpers/Mail/GmailTransportManager.php b/app/Helpers/Mail/GmailTransportManager.php index 3db322048b..b6f574427f 100644 --- a/app/Helpers/Mail/GmailTransportManager.php +++ b/app/Helpers/Mail/GmailTransportManager.php @@ -10,9 +10,10 @@ */ namespace App\Helpers\Mail; -use Illuminate\Mail\MailManager; use App\CustomMailDriver\CustomTransport; +use App\Helpers\Mail\Office365MailTransport; use Dacastro4\LaravelGmail\Services\Message\Mail; +use Illuminate\Mail\MailManager; use Illuminate\Support\Facades\Config; @@ -22,4 +23,9 @@ class GmailTransportManager extends MailManager { return new GmailTransport(new Mail); } + + protected function createOffice365Transport() + { + return new Office365MailTransport(); + } } \ No newline at end of file diff --git a/app/Helpers/Mail/Office365MailTransport.php b/app/Helpers/Mail/Office365MailTransport.php new file mode 100644 index 0000000000..b7fa2546f6 --- /dev/null +++ b/app/Helpers/Mail/Office365MailTransport.php @@ -0,0 +1,301 @@ +beforeSendPerformed($message); + + $graph = new Graph(); + $token = $message->getHeaders()->get('GmailToken')->getValue(); + + $graph->setAccessToken($token); + + // Special treatment if the message has too large attachments + $messageBody = $this->getBody($message, true); + $messageBodySizeMb = json_encode($messageBody); + $messageBodySizeMb = strlen($messageBodySizeMb); + $messageBodySizeMb = $messageBodySizeMb / 1048576; //byte -> mb + + if ($messageBodySizeMb >= 4) { + unset($messageBody); + $graphMessage = $graph->createRequest("POST", "/users/" . key($message->getFrom()) . "/messages") + ->attachBody($this->getBody($message)) + ->setReturnType(\Microsoft\Graph\Model\Message::class) + ->execute(); + + foreach ($message->getChildren() as $attachment) { + if ($attachment instanceof \Swift_Mime_SimpleMimeEntity) { + $fileName = $attachment->getHeaders()->get('Content-Type')->getParameter('name'); + $content = $attachment->getBody(); + $fileSize = strlen($content); + $size = $fileSize / 1048576; //byte -> mb + $id = $attachment->getId(); + $attachmentMessage = [ + 'AttachmentItem' => [ + 'attachmentType' => 'file', + 'name' => $fileName, + 'size' => strlen($content) + ] + ]; + + if ($size <= 3) { //ErrorAttachmentSizeShouldNotBeLessThanMinimumSize if attachment <= 3mb, then we need to add this + $attachmentBody = [ + "@odata.type" => "#microsoft.graph.fileAttachment", + "name" => $attachment->getHeaders()->get('Content-Type')->getParameter('name'), + "contentType" => $attachment->getBodyContentType(), + "contentBytes" => base64_encode($attachment->getBody()), + 'contentId' => $id + ]; + + $addAttachment = $graph->createRequest("POST", "/users/" . key($message->getFrom()) . "/messages/" . $graphMessage->getId() . "/attachments") + ->attachBody($attachmentBody) + ->setReturnType(UploadSession::class) + ->execute(); + } else { + //upload the files in chunks of 4mb.... + $uploadSession = $graph->createRequest("POST", "/users/" . key($message->getFrom()) . "/messages/" . $graphMessage->getId() . "/attachments/createUploadSession") + ->attachBody($attachmentMessage) + ->setReturnType(UploadSession::class) + ->execute(); + + $fragSize = 1024 * 1024 * 4; //4mb at once... + $numFragments = ceil($fileSize / $fragSize); + $contentChunked = str_split($content, $fragSize); + $bytesRemaining = $fileSize; + + $i = 0; + while ($i < $numFragments) { + $chunkSize = $numBytes = $fragSize; + $start = $i * $fragSize; + $end = $i * $fragSize + $chunkSize - 1; + if ($bytesRemaining < $chunkSize) { + $chunkSize = $numBytes = $bytesRemaining; + $end = $fileSize - 1; + } + $data = $contentChunked[$i]; + $content_range = "bytes " . $start . "-" . $end . "/" . $fileSize; + $headers = [ + "Content-Length" => $numBytes, + "Content-Range" => $content_range + ]; + $client = new \GuzzleHttp\Client(); + $tmp = $client->put($uploadSession->getUploadUrl(), [ + 'headers' => $headers, + 'body' => $data, + 'allow_redirects' => false, + 'timeout' => 1000 + ]); + $result = $tmp->getBody() . ''; + $result = json_decode($result); //if body == empty, then the file was successfully uploaded + $bytesRemaining = $bytesRemaining - $chunkSize; + $i++; + } + } + } + } + + //definetly send the message + $graph->createRequest("POST", "/users/" . key($message->getFrom()) . "/messages/" . $graphMessage->getId() . "/send")->execute(); + } else { + + try { + $graphMessage = $graph->createRequest("POST", "/users/" . key($message->getFrom()) . "/sendmail") + ->attachBody($messageBody) + ->setReturnType(\Microsoft\Graph\Model\Message::class) + ->execute(); + } + catch(\Exception $e){ + + sleep(5); + $graphMessage = $graph->createRequest("POST", "/users/" . key($message->getFrom()) . "/sendmail") + ->attachBody($messageBody) + ->setReturnType(\Microsoft\Graph\Model\Message::class) + ->execute(); + } + } + + $this->sendPerformed($message); + + return $this->numberOfRecipients($message); + } + + /** + * Get body for the message. + * + * @param \Swift_Mime_SimpleMessage $message + * @param bool $withAttachments + * @return array + */ + + protected function getBody(Swift_Mime_SimpleMessage $message, $withAttachments = false) + { + $messageData = [ + 'from' => [ + 'emailAddress' => [ + 'address' => key($message->getFrom()), + 'name' => current($message->getFrom()) + ] + ], + 'toRecipients' => $this->getTo($message), + 'ccRecipients' => $this->getCc($message), + 'bccRecipients' => $this->getBcc($message), + 'replyTo' => $this->getReplyTo($message), + 'subject' => $message->getSubject(), + 'body' => [ + 'contentType' => $message->getBodyContentType() == "text/html" ? 'html' : 'text', + 'content' => $message->getBody() + ] + ]; + + if ($withAttachments) { + $messageData = ['message' => $messageData]; + //add attachments if any + $attachments = []; + foreach ($message->getChildren() as $attachment) { + if ($attachment instanceof \Swift_Mime_SimpleMimeEntity && $attachment->getContentType() != 'text/plain') { + $attachments[] = [ + "@odata.type" => "#microsoft.graph.fileAttachment", + "name" => $attachment->getHeaders()->get('Content-Type')->getParameter('name'), + "contentType" => $attachment->getBodyContentType(), + "contentBytes" => base64_encode($attachment->getBody()), + 'contentId' => $attachment->getId() + ]; + } + } + if (count($attachments) > 0) { + $messageData['message']['attachments'] = $attachments; + } + } + + return $messageData; + } + + /** + * Get the "to" payload field for the API request. + * + * @param \Swift_Mime_SimpleMessage $message + * @return string + */ + protected function getTo(Swift_Mime_SimpleMessage $message) + { + return collect((array) $message->getTo())->map(function ($display, $address) { + return $display ? [ + 'emailAddress' => [ + 'address' => $address, + 'name' => $display + ] + ] : [ + 'emailAddress' => [ + 'address' => $address + ] + ]; + })->values()->toArray(); + } + + /** + * Get the "Cc" payload field for the API request. + * + * @param \Swift_Mime_SimpleMessage $message + * @return string + */ + protected function getCc(Swift_Mime_SimpleMessage $message) + { + return collect((array) $message->getCc())->map(function ($display, $address) { + return $display ? [ + 'emailAddress' => [ + 'address' => $address, + 'name' => $display + ] + ] : [ + 'emailAddress' => [ + 'address' => $address + ] + ]; + })->values()->toArray(); + } + + /** + * Get the "replyTo" payload field for the API request. + * + * @param \Swift_Mime_SimpleMessage $message + * @return string + */ + protected function getReplyTo(Swift_Mime_SimpleMessage $message) + { + return collect((array) $message->getReplyTo())->map(function ($display, $address) { + return $display ? [ + 'emailAddress' => [ + 'address' => $address, + 'name' => $display + ] + ] : [ + 'emailAddress' => [ + 'address' => $address + ] + ]; + })->values()->toArray(); + } + + /** + * Get the "Bcc" payload field for the API request. + * + * @param \Swift_Mime_SimpleMessage $message + * @return string + */ + protected function getBcc(Swift_Mime_SimpleMessage $message) + { + return collect((array) $message->getBcc())->map(function ($display, $address) { + return $display ? [ + 'emailAddress' => [ + 'address' => $address, + 'name' => $display + ] + ] : [ + 'emailAddress' => [ + 'address' => $address + ] + ]; + })->values()->toArray(); + } + + /** + * Get all of the contacts for the message. + * + * @param \Swift_Mime_SimpleMessage $message + * @return array + */ + protected function allContacts(Swift_Mime_SimpleMessage $message) + { + return array_merge( + (array) $message->getTo(), + (array) $message->getCc(), + (array) $message->getBcc(), + (array) $message->getReplyTo() + ); + } + +} \ No newline at end of file diff --git a/app/Helpers/Mail/Office365TransportManager.php b/app/Helpers/Mail/Office365TransportManager.php new file mode 100644 index 0000000000..3c88383820 --- /dev/null +++ b/app/Helpers/Mail/Office365TransportManager.php @@ -0,0 +1,23 @@ +fresh(); } - // $user->setCompany($user->account->default_company); - // $this->setLoginCache($user); - - // $cu = CompanyUser::query() - // ->where('user_id', auth()->user()->id); - $cu = $this->hydrateCompanyUser(); if($cu->count() == 0) return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // $truth = app()->make(TruthSource::class); - - // $truth->setCompanyUser($cu->first()); - // $truth->setUser(auth()->user()); - // $truth->setCompany($user->account->default_company); - - // if(!$cu->exists()) - // return response()->json(['message' => 'User not linked to any companies'], 403); - - // /* Ensure the user has a valid token */ - // if($user->company_users()->count() != $user->tokens()->count()) - // { - - // $user->companies->each(function($company) use($user, $request){ - - // if(!CompanyToken::where('user_id', $user->id)->where('company_id', $company->id)->exists()){ - - // CreateCompanyToken::dispatchNow($company, $user, $request->server('HTTP_USER_AGENT')); - - // } - - // }); - - // } - - // $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $user->account->default_company->id)->first()); - /*On the hosted platform, only owners can login for free/pro accounts*/ if (Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); @@ -363,17 +332,18 @@ class LoginController extends BaseController if (request()->input('provider') == 'google') { return $this->handleGoogleOauth(); } elseif (request()->input('provider') == 'microsoft') { - if (request()->has('token')) { - return $this->handleSocialiteLogin('microsoft', request()->get('token')); - } else { - $message = 'Bearer token missing for the microsoft login'; - } + // if (request()->has('token')) { + // return $this->handleSocialiteLogin('microsoft', request()->get('token')); + // } else { + // $message = 'Bearer token missing for the microsoft login'; + // } + return $this->handleMicrosoftOauth(); } elseif (request()->input('provider') == 'apple') { - if (request()->has('token')) { - return $this->handleSocialiteLogin('apple', request()->get('token')); - } else { - $message = 'Token is missing for the apple login'; - } + // if (request()->has('token')) { + // return $this->handleSocialiteLogin('apple', request()->get('token')); + // } else { + // $message = 'Token is missing for the apple login'; + // } } return response() @@ -483,6 +453,9 @@ class LoginController extends BaseController $cu = CompanyUser::query()->where('user_id', auth()->user()->id); + if($cu->count() == 0) + return $cu; + if (CompanyUser::query()->where('user_id', auth()->user()->id)->where('company_id', auth()->user()->account->default_company_id)->exists()) $set_company = auth()->user()->account->default_company; else { @@ -509,7 +482,7 @@ class LoginController extends BaseController CreateCompanyToken::dispatchNow($company, auth()->user(), "Google_O_Auth"); - } + } }); @@ -521,6 +494,100 @@ class LoginController extends BaseController } + private function handleMicrosoftOauth() + { + if(request()->has('accessToken')) + $accessToken = request()->input('accessToken'); + else + return response()->json(['message' => 'Invalid response from oauth server'], 400); + + $graph = new \Microsoft\Graph\Graph(); + $graph->setAccessToken($accessToken); + + $user = $graph->createRequest("GET", "/me") + ->setReturnType(Model\User::class) + ->execute(); + + if($user){ + + $account = request()->input('account'); + $email = $user->getMail() ?: $user->getUserPrincipalName(); + + $query = [ + 'oauth_user_id' => $user->getId(), + 'oauth_provider_id'=> 'microsoft', + ]; + + if ($existing_user = MultiDB::hasUser($query)) { + + if(!$existing_user->account) + return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); + + return $this->existingOauthUser($existing_user); + } + + //If this is a result user/email combo - lets add their OAuth details details + if($existing_login_user = MultiDB::hasUser(['email' => $email])) + { + if(!$existing_login_user->account) + return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); + + Auth::login($existing_login_user, true); + + return $this->existingLoginUser($user->getId(), 'microsoft'); + } + + // Signup! + $new_account = [ + 'first_name' => $user->getGivenName() ?: '', + 'last_name' => $user->getSurname() ?: '' , + 'password' => '', + 'email' => $email, + 'oauth_user_id' => $user->getId(), + 'oauth_provider_id' => 'microsoft', + ]; + + return $this->createNewAccount($new_account); + + } + + } + + private function existingOauthUser($existing_user) + { + Auth::login($existing_user, true); + + $cu = $this->hydrateCompanyUser(); + + if($cu->count() == 0) + return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + + if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_user->account->isEnterpriseClient()) + return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + + return $this->timeConstrainedResponse($cu); + } + + private function existingLoginUser($oauth_user_id, $provider) + { + + auth()->user()->update([ + 'oauth_user_id' => $oauth_user_id, + 'oauth_provider_id'=> $provider, + ]); + + $cu = $this->hydrateCompanyUser(); + + if($cu->count() == 0) + return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + + if(Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient()) + return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + + return $this->timeConstrainedResponse($cu); + + } + private function handleGoogleOauth() { $user = false; @@ -541,18 +608,7 @@ class LoginController extends BaseController if(!$existing_user->account) return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); - Auth::login($existing_user, true); - - $cu = $this->hydrateCompanyUser(); - - if($cu->count() == 0) - return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - - if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_user->account->isEnterpriseClient()) - return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - - return $this->timeConstrainedResponse($cu); - + return $this->existingOauthUser($existing_user); } //If this is a result user/email combo - lets add their OAuth details details @@ -563,20 +619,7 @@ class LoginController extends BaseController Auth::login($existing_login_user, true); - auth()->user()->update([ - 'oauth_user_id' => $google->harvestSubField($user), - 'oauth_provider_id'=> 'google', - ]); - - $cu = $this->hydrateCompanyUser(); - - if($cu->count() == 0) - return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - - if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) - return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - - return $this->timeConstrainedResponse($cu); + return $this->existingLoginUser($google->harvestSubField($user), 'google'); } } @@ -584,7 +627,6 @@ class LoginController extends BaseController if ($user) { //check the user doesn't already exist in some form - if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)])) { if(!$existing_login_user->account) @@ -592,26 +634,9 @@ class LoginController extends BaseController Auth::login($existing_login_user, true); - auth()->user()->update([ - 'oauth_user_id' => $google->harvestSubField($user), - 'oauth_provider_id'=> 'google', - ]); - - $cu = $this->hydrateCompanyUser(); - - // $cu = CompanyUser::query() - // ->where('user_id', auth()->user()->id); - - if ($cu->count() == 0) - return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - - if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) - return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - - return $this->timeConstrainedResponse($cu); + return $this->existingLoginUser($google->harvestSubField($user), 'google'); } - //user not found anywhere - lets sign them up. $name = OAuth::splitName($google->harvestName($user)); @@ -624,23 +649,7 @@ class LoginController extends BaseController 'oauth_provider_id' => 'google', ]; - MultiDB::setDefaultDatabase(); - - $account = CreateAccount::dispatchNow($new_account, request()->getClientIp()); - - Auth::login($account->default_company->owner(), true); - auth()->user()->email_verified_at = now(); - auth()->user()->save(); - - $cu = $this->hydrateCompanyUser(); - - if($cu->count() == 0) - return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - - if(Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient()) - return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); - - return $this->timeConstrainedResponse($cu); + return $this->createNewAccount($new_account); } return response() @@ -649,6 +658,32 @@ class LoginController extends BaseController ->header('X-Api-Version', config('ninja.minimum_client_version')); } + private function createNewAccount($new_account) + { + + MultiDB::setDefaultDatabase(); + + $account = CreateAccount::dispatchNow($new_account, request()->getClientIp()); + + if(!$account instanceOf Account) + return $account; + + Auth::login($account->default_company->owner(), true); + auth()->user()->email_verified_at = now(); + auth()->user()->save(); + + $cu = $this->hydrateCompanyUser(); + + if($cu->count() == 0) + return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); + + if(Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient()) + return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); + + return $this->timeConstrainedResponse($cu); + + } + public function redirectToProvider(string $provider) { @@ -662,11 +697,16 @@ class LoginController extends BaseController $parameters = ['access_type' => 'offline', "prompt" => "consent select_account", 'redirect_uri' => config('ninja.app_url')."/auth/google"]; } + if($provider == 'microsoft'){ + $scopes = ['email', 'Mail.ReadWrite', 'Mail.Send', 'offline_access', 'profile', 'User.Read openid']; + $parameters = ['response_type' => 'code', 'redirect_uri' => config('ninja.app_url')."/auth/microsoft"]; + } + if (request()->has('code')) { return $this->handleProviderCallback($provider); } else { - if(!in_array($provider, ['google'])) + if(!in_array($provider, ['google','microsoft'])) return abort(400, 'Invalid provider'); return Socialite::driver($provider)->with($parameters)->scopes($scopes)->redirect(); @@ -675,6 +715,10 @@ class LoginController extends BaseController public function handleProviderCallback(string $provider) { + + if($provider == 'microsoft') + return $this->handleMicrosoftProviderCallback(); + $socialite_user = Socialite::driver($provider)->user(); $oauth_user_token = ''; @@ -714,4 +758,43 @@ class LoginController extends BaseController return redirect('/#/'); } + + public function handleMicrosoftProviderCallback($provider = 'microsoft') + { + + $socialite_user = Socialite::driver($provider)->user(); + nlog($socialite_user); + + nlog("refresh token " . $socialite_user->accessTokenResponseBody['refresh_token']); + nlog("access token " . $socialite_user->accessTokenResponseBody['access_token']); + + $oauth_user_token = $socialite_user->accessTokenResponseBody['access_token']; + + if($user = OAuth::handleAuth($socialite_user, $provider)) + { + + nlog('found user and updating their user record'); + $name = OAuth::splitName($socialite_user->getName()); + + $update_user = [ + 'first_name' => $name[0], + 'last_name' => $name[1], + 'email' => $socialite_user->getEmail(), + 'oauth_user_id' => $socialite_user->getId(), + 'oauth_provider_id' => $provider, + 'oauth_user_token' => $oauth_user_token, + 'oauth_user_refresh_token' => $socialite_user->accessTokenResponseBody['refresh_token'] + ]; + + $user->update($update_user); + + } + else { + nlog("user not found for oauth"); + } + + return redirect('/#/'); + + } + } diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 1595b5483f..490a60f31a 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -80,7 +80,8 @@ class InvitationController extends Controller $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; - $invitation = $entity_obj::where('key', $invitation_key) + $invitation = $entity_obj::withTrashed() + ->where('key', $invitation_key) ->whereHas($entity, function ($query) { $query->where('is_deleted',0); }) @@ -186,7 +187,8 @@ class InvitationController extends Controller $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; - $invitation = $entity_obj::where('key', $invitation_key) + $invitation = $entity_obj::withTrashed() + ->where('key', $invitation_key) ->with('contact.client') ->firstOrFail(); @@ -228,7 +230,8 @@ class InvitationController extends Controller public function payInvoice(Request $request, string $invitation_key) { - $invitation = InvoiceInvitation::where('key', $invitation_key) + $invitation = InvoiceInvitation::withTrashed() + ->where('key', $invitation_key) ->with('contact.client') ->firstOrFail(); diff --git a/app/Http/Controllers/LogoutController.php b/app/Http/Controllers/LogoutController.php index 2d0c0f4e4e..5a5a301505 100644 --- a/app/Http/Controllers/LogoutController.php +++ b/app/Http/Controllers/LogoutController.php @@ -60,7 +60,7 @@ class LogoutController extends BaseController public function index(Request $request) { $ct = CompanyToken::with('company.tokens') - ->whereRaw('BINARY `token`= ?', [$request->header('X-API-TOKEN')]) + ->where('token', $request->header('X-API-TOKEN')) ->first(); $ct->company diff --git a/app/Http/Controllers/VendorPortal/InvitationController.php b/app/Http/Controllers/VendorPortal/InvitationController.php index 0d3d730f4b..9f5f6ac93b 100644 --- a/app/Http/Controllers/VendorPortal/InvitationController.php +++ b/app/Http/Controllers/VendorPortal/InvitationController.php @@ -46,7 +46,8 @@ class InvitationController extends Controller Auth::logout(); - $invitation = PurchaseOrderInvitation::where('key', $invitation_key) + $invitation = PurchaseOrderInvitation::withTrashed() + ->where('key', $invitation_key) ->whereHas('purchase_order', function ($query) { $query->where('is_deleted',0); }) diff --git a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php index 8b0b98e076..d3c4b3c651 100644 --- a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php +++ b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php @@ -141,6 +141,8 @@ class PurchaseOrderController extends Controller ->whereIn('id', $this->transformKeys($data['purchase_orders'])) ->where('company_id', auth()->guard('vendor')->user()->vendor->company_id) ->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT]) + ->where('is_deleted', 0) + ->withTrashed() ->cursor()->each(function ($purchase_order){ $purchase_order->service() @@ -159,7 +161,7 @@ class PurchaseOrderController extends Controller if(count($data['purchase_orders']) == 1){ - $purchase_order = PurchaseOrder::whereIn('id', $this->transformKeys($data['purchase_orders']))->first(); + $purchase_order = PurchaseOrder::withTrashed()->where('is_deleted', 0)->whereIn('id', $this->transformKeys($data['purchase_orders']))->first(); return redirect()->route('vendor.purchase_order.show', ['purchase_order' => $purchase_order->hashed_id]); diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 80487f2491..2a2a7ee22a 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -184,12 +184,49 @@ class NinjaMailerJob implements ShouldQueue $this->mailer = 'gmail'; $this->setGmailMailer(); break; + case 'office365': + $this->mailer = 'office365'; + $this->setOfficeMailer(); + break; default: break; } } + private function setOfficeMailer() + { + $sending_user = $this->nmo->settings->gmail_sending_user_id; + + $user = User::find($this->decodePrimaryKey($sending_user)); + + nlog("Sending via {$user->name()}"); + + $token = $this->refreshOfficeToken($user); + + if($token) + { + $user->oauth_user_token = $token; + $user->save(); + + } + else { + + $this->nmo->settings->email_sending_method = 'default'; + return $this->setMailDriver(); + + } + + $this->nmo + ->mailable + ->from($user->email, $user->name()) + ->withSwiftMessage(function ($message) use($token) { + $message->getHeaders()->addTextHeader('GmailToken', $token); + }); + + sleep(rand(1,3)); + } + private function setGmailMailer() { if(LaravelGmail::check()) @@ -303,4 +340,25 @@ class NinjaMailerJob implements ShouldQueue } + private function refreshOfficeToken($user) + { + $guzzle = new \GuzzleHttp\Client(); + $url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + + $token = json_decode($guzzle->post($url, [ + 'form_params' => [ + 'client_id' => config('ninja.o365.client_id') , + 'client_secret' => config('ninja.o365.client_secret') , + 'scope' => 'email Mail.ReadWrite Mail.Send offline_access profile User.Read openid', + 'grant_type' => 'refresh_token', + 'refresh_token' => $user->oauth_user_refresh_token + ], + ])->getBody()->getContents()); + + if($token) + return $token->access_token; + + return false; + } + } \ No newline at end of file diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index c12d829ae1..86ae1adbf5 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -88,8 +88,14 @@ class TemplateEmail extends Mailable $this->from(config('mail.from.address'), $email_from_name); - if (strlen($settings->bcc_email) > 1) - $this->bcc(explode(",",str_replace(" ", "", $settings->bcc_email)));//remove whitespace if any has been inserted. + if (strlen($settings->bcc_email) > 1){ + + if(Ninja::isHosted()) + $this->bcc(reset(explode(",",str_replace(" ", "", $settings->bcc_email))));//remove whitespace if any has been inserted. + else + $this->bcc(explode(",",str_replace(" ", "", $settings->bcc_email)));//remove whitespace if any has been inserted. + + } $this->subject($this->build_email->getSubject()) ->text('email.template.text', [ diff --git a/app/Models/Account.php b/app/Models/Account.php index 2aef2603d7..0ad6674c9f 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -374,6 +374,9 @@ class Account extends BaseModel public function getDailyEmailLimit() { + if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0) + return 50; + if($this->isPaid()){ $limit = $this->paid_plan_email_quota; $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100; diff --git a/app/Providers/MailServiceProvider.php b/app/Providers/MailServiceProvider.php index 3389f5a4cc..b9489888d6 100644 --- a/app/Providers/MailServiceProvider.php +++ b/app/Providers/MailServiceProvider.php @@ -39,7 +39,6 @@ class MailServiceProvider extends MailProvider return new GmailTransportManager($app); }); - //this is octane ready - but is untested // $this->app->bind('mail.manager', function ($app){ // return new GmailTransportManager($app); diff --git a/app/Transformers/CreditTransformer.php b/app/Transformers/CreditTransformer.php index 296778058e..42e247d1c3 100644 --- a/app/Transformers/CreditTransformer.php +++ b/app/Transformers/CreditTransformer.php @@ -18,6 +18,7 @@ use App\Models\CreditInvitation; use App\Models\Document; use App\Transformers\ActivityTransformer; use App\Utils\Traits\MakesHash; +use League\Fractal\Resource\Item; class CreditTransformer extends EntityTransformer { @@ -30,6 +31,7 @@ class CreditTransformer extends EntityTransformer protected $availableIncludes = [ 'activities', + 'client', ]; public function includeActivities(Credit $credit) @@ -53,28 +55,13 @@ class CreditTransformer extends EntityTransformer return $this->includeCollection($credit->invitations, $transformer, CreditInvitation::class); } - /* - public function includePayments(quote $credit) - { - $transformer = new PaymentTransformer($this->account, $this->serializer, $credit); + public function includeClient(Credit $credit): Item + { + $transformer = new ClientTransformer($this->serializer); - return $this->includeCollection($credit->payments, $transformer, ENTITY_PAYMENT); - } + return $this->includeItem($credit->client, $transformer, Client::class); + } - public function includeClient(quote $credit) - { - $transformer = new ClientTransformer($this->account, $this->serializer); - - return $this->includeItem($credit->client, $transformer, ENTITY_CLIENT); - } - - public function includeExpenses(quote $credit) - { - $transformer = new ExpenseTransformer($this->account, $this->serializer); - - return $this->includeCollection($credit->expenses, $transformer, ENTITY_EXPENSE); - } -*/ public function includeDocuments(Credit $credit) { $transformer = new DocumentTransformer($this->serializer); diff --git a/composer.json b/composer.json index bbcce36e61..c67ba67be2 100644 --- a/composer.json +++ b/composer.json @@ -71,6 +71,7 @@ "league/fractal": "^0.17.0", "league/omnipay": "^3.1", "livewire/livewire": "^2.6", + "microsoft/microsoft-graph": "^1.69", "mollie/mollie-api-php": "^2.36", "nelexa/zip": "^4.0", "nwidart/laravel-modules": "8.3", diff --git a/composer.lock b/composer.lock index 049fe7120a..36f2634ab1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6845489fdc254427c4536e22f025ff51", + "content-hash": "df84a1903809a8e781d937e679821e74", "packages": [ { "name": "afosto/yaac", @@ -4928,6 +4928,57 @@ ], "time": "2022-04-07T21:38:12+00:00" }, + { + "name": "microsoft/microsoft-graph", + "version": "1.69.0", + "source": { + "type": "git", + "url": "https://github.com/microsoftgraph/msgraph-sdk-php.git", + "reference": "dc867afdb2c89ea7ead37d6bcfcaf0389e7a85f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/microsoftgraph/msgraph-sdk-php/zipball/dc867afdb2c89ea7ead37d6bcfcaf0389e7a85f4", + "reference": "dc867afdb2c89ea7ead37d6bcfcaf0389e7a85f4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": "^8.0 || ^7.3", + "psr/http-message": "^1.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.2", + "phpstan/phpstan": "^0.12.90 || ^1.0.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Microsoft\\Graph\\": "src/", + "Beta\\Microsoft\\Graph\\": "src/Beta/Microsoft/Graph/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Microsoft Graph Client Tooling", + "email": "graphtooling@service.microsoft.com", + "role": "Developer" + } + ], + "description": "The Microsoft Graph SDK for PHP", + "homepage": "https://developer.microsoft.com/en-us/graph", + "support": { + "issues": "https://github.com/microsoftgraph/msgraph-sdk-php/issues", + "source": "https://github.com/microsoftgraph/msgraph-sdk-php/tree/1.69.0" + }, + "time": "2022-06-15T11:11:33+00:00" + }, { "name": "mollie/mollie-api-php", "version": "v2.44.1", diff --git a/config/mail.php b/config/mail.php index 88b35739aa..97ab1b8c18 100644 --- a/config/mail.php +++ b/config/mail.php @@ -73,6 +73,9 @@ return [ 'gmail' => [ 'transport' => 'gmail', ], + 'office365' => [ + 'transport' => 'office365', + ], 'cocopostmark' => [ 'transport' => 'cocopostmark', ], diff --git a/config/ninja.php b/config/ninja.php index a89d2456c5..4d911b56bf 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.4.2', - 'app_tag' => '5.4.2', + 'app_version' => '5.4.3', + 'app_tag' => '5.4.3', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), @@ -155,6 +155,11 @@ return [ 'designs' => [ 'base_path' => resource_path('views/pdf-designs/'), ], + 'o365' => [ + 'client_secret' => env('MICROSOFT_CLIENT_SECRET', false), + 'client_id' => env('MICROSOFT_CLIENT_ID', false), + 'tenant_id' => env('MICROSOFT_TENANT_ID', false), + ], 'maintenance' => [ 'delete_pdfs' => env('DELETE_PDF_DAYS', 0), 'delete_backups' => env('DELETE_BACKUP_DAYS', 0), diff --git a/database/migrations/2022_06_17_082627_change_refresh_token_column_size.php b/database/migrations/2022_06_17_082627_change_refresh_token_column_size.php new file mode 100644 index 0000000000..9926ae6ab2 --- /dev/null +++ b/database/migrations/2022_06_17_082627_change_refresh_token_column_size.php @@ -0,0 +1,33 @@ +text('oauth_user_refresh_token')->change(); + }); + + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}