1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-23 01:41:34 +02:00

Merge pull request #7569 from turbo124/v5-stable

v5.4.3
This commit is contained in:
David Bomba 2022-06-18 09:00:02 +10:00 committed by GitHub
commit 73940eab06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 707 additions and 142 deletions

View File

@ -1 +1 @@
5.4.2 5.4.3

View File

@ -163,7 +163,7 @@ class CompanySettings extends BaseSettings
public $require_quote_signature = false; //@TODO ben to confirm public $require_quote_signature = false; //@TODO ben to confirm
//email settings //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 $gmail_sending_user_id = '0'; //@implemented
public $reply_to_email = ''; //@implemented public $reply_to_email = ''; //@implemented

View File

@ -10,9 +10,10 @@
*/ */
namespace App\Helpers\Mail; namespace App\Helpers\Mail;
use Illuminate\Mail\MailManager;
use App\CustomMailDriver\CustomTransport; use App\CustomMailDriver\CustomTransport;
use App\Helpers\Mail\Office365MailTransport;
use Dacastro4\LaravelGmail\Services\Message\Mail; use Dacastro4\LaravelGmail\Services\Message\Mail;
use Illuminate\Mail\MailManager;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
@ -22,4 +23,9 @@ class GmailTransportManager extends MailManager
{ {
return new GmailTransport(new Mail); return new GmailTransport(new Mail);
} }
protected function createOffice365Transport()
{
return new Office365MailTransport();
}
} }

View File

@ -0,0 +1,301 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Mail;
use Illuminate\Mail\Transport\Transport;
use Illuminate\Support\Str;
use Swift_Mime_SimpleMessage;
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model\UploadSession;
class Office365MailTransport extends Transport
{
public function __construct()
{
}
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
$this->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()
);
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Mail;
use App\Helpers\Mail\Office365MailTransport;
use Illuminate\Mail\MailManager;
class Office365TransportManager extends MailManager
{
protected function createOffice365Transport()
{
return new Office365MailTransport();
}
}

View File

@ -23,6 +23,7 @@ use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Libraries\OAuth\OAuth; use App\Libraries\OAuth\OAuth;
use App\Libraries\OAuth\Providers\Google; use App\Libraries\OAuth\Providers\Google;
use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\Company; use App\Models\Company;
use App\Models\CompanyToken; use App\Models\CompanyToken;
@ -44,6 +45,7 @@ use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Facades\Socialite;
use PragmaRX\Google2FA\Google2FA; use PragmaRX\Google2FA\Google2FA;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
use Microsoft\Graph\Model;
class LoginController extends BaseController class LoginController extends BaseController
{ {
@ -211,44 +213,11 @@ class LoginController extends BaseController
$user = $user->fresh(); $user = $user->fresh();
} }
// $user->setCompany($user->account->default_company);
// $this->setLoginCache($user);
// $cu = CompanyUser::query()
// ->where('user_id', auth()->user()->id);
$cu = $this->hydrateCompanyUser(); $cu = $this->hydrateCompanyUser();
if($cu->count() == 0) if($cu->count() == 0)
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); 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*/ /*On the hosted platform, only owners can login for free/pro accounts*/
if (Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient()) 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); 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') { if (request()->input('provider') == 'google') {
return $this->handleGoogleOauth(); return $this->handleGoogleOauth();
} elseif (request()->input('provider') == 'microsoft') { } elseif (request()->input('provider') == 'microsoft') {
if (request()->has('token')) { // if (request()->has('token')) {
return $this->handleSocialiteLogin('microsoft', request()->get('token')); // return $this->handleSocialiteLogin('microsoft', request()->get('token'));
} else { // } else {
$message = 'Bearer token missing for the microsoft login'; // $message = 'Bearer token missing for the microsoft login';
} // }
return $this->handleMicrosoftOauth();
} elseif (request()->input('provider') == 'apple') { } elseif (request()->input('provider') == 'apple') {
if (request()->has('token')) { // if (request()->has('token')) {
return $this->handleSocialiteLogin('apple', request()->get('token')); // return $this->handleSocialiteLogin('apple', request()->get('token'));
} else { // } else {
$message = 'Token is missing for the apple login'; // $message = 'Token is missing for the apple login';
} // }
} }
return response() return response()
@ -483,6 +453,9 @@ class LoginController extends BaseController
$cu = CompanyUser::query()->where('user_id', auth()->user()->id); $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()) 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; $set_company = auth()->user()->account->default_company;
else { else {
@ -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() private function handleGoogleOauth()
{ {
$user = false; $user = false;
@ -541,18 +608,7 @@ class LoginController extends BaseController
if(!$existing_user->account) if(!$existing_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_user, true); return $this->existingOauthUser($existing_user);
$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);
} }
//If this is a result user/email combo - lets add their OAuth details details //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::login($existing_login_user, true);
auth()->user()->update([ return $this->existingLoginUser($google->harvestSubField($user), 'google');
'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);
} }
} }
@ -584,7 +627,6 @@ class LoginController extends BaseController
if ($user) { if ($user) {
//check the user doesn't already exist in some form //check the user doesn't already exist in some form
if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)])) if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)]))
{ {
if(!$existing_login_user->account) if(!$existing_login_user->account)
@ -592,26 +634,9 @@ class LoginController extends BaseController
Auth::login($existing_login_user, true); Auth::login($existing_login_user, true);
auth()->user()->update([ return $this->existingLoginUser($google->harvestSubField($user), 'google');
'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);
} }
//user not found anywhere - lets sign them up. //user not found anywhere - lets sign them up.
$name = OAuth::splitName($google->harvestName($user)); $name = OAuth::splitName($google->harvestName($user));
@ -624,10 +649,25 @@ class LoginController extends BaseController
'oauth_provider_id' => 'google', 'oauth_provider_id' => 'google',
]; ];
return $this->createNewAccount($new_account);
}
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
private function createNewAccount($new_account)
{
MultiDB::setDefaultDatabase(); MultiDB::setDefaultDatabase();
$account = CreateAccount::dispatchNow($new_account, request()->getClientIp()); $account = CreateAccount::dispatchNow($new_account, request()->getClientIp());
if(!$account instanceOf Account)
return $account;
Auth::login($account->default_company->owner(), true); Auth::login($account->default_company->owner(), true);
auth()->user()->email_verified_at = now(); auth()->user()->email_verified_at = now();
auth()->user()->save(); auth()->user()->save();
@ -641,12 +681,7 @@ class LoginController extends BaseController
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
return $this->timeConstrainedResponse($cu); return $this->timeConstrainedResponse($cu);
}
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
public function redirectToProvider(string $provider) 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"]; $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')) { if (request()->has('code')) {
return $this->handleProviderCallback($provider); return $this->handleProviderCallback($provider);
} else { } else {
if(!in_array($provider, ['google'])) if(!in_array($provider, ['google','microsoft']))
return abort(400, 'Invalid provider'); return abort(400, 'Invalid provider');
return Socialite::driver($provider)->with($parameters)->scopes($scopes)->redirect(); return Socialite::driver($provider)->with($parameters)->scopes($scopes)->redirect();
@ -675,6 +715,10 @@ class LoginController extends BaseController
public function handleProviderCallback(string $provider) public function handleProviderCallback(string $provider)
{ {
if($provider == 'microsoft')
return $this->handleMicrosoftProviderCallback();
$socialite_user = Socialite::driver($provider)->user(); $socialite_user = Socialite::driver($provider)->user();
$oauth_user_token = ''; $oauth_user_token = '';
@ -714,4 +758,43 @@ class LoginController extends BaseController
return redirect('/#/'); 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('/#/');
}
} }

View File

@ -80,7 +80,8 @@ class InvitationController extends Controller
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; $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) { ->whereHas($entity, function ($query) {
$query->where('is_deleted',0); $query->where('is_deleted',0);
}) })
@ -186,7 +187,8 @@ class InvitationController extends Controller
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; $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') ->with('contact.client')
->firstOrFail(); ->firstOrFail();
@ -228,7 +230,8 @@ class InvitationController extends Controller
public function payInvoice(Request $request, string $invitation_key) public function payInvoice(Request $request, string $invitation_key)
{ {
$invitation = InvoiceInvitation::where('key', $invitation_key) $invitation = InvoiceInvitation::withTrashed()
->where('key', $invitation_key)
->with('contact.client') ->with('contact.client')
->firstOrFail(); ->firstOrFail();

View File

@ -60,7 +60,7 @@ class LogoutController extends BaseController
public function index(Request $request) public function index(Request $request)
{ {
$ct = CompanyToken::with('company.tokens') $ct = CompanyToken::with('company.tokens')
->whereRaw('BINARY `token`= ?', [$request->header('X-API-TOKEN')]) ->where('token', $request->header('X-API-TOKEN'))
->first(); ->first();
$ct->company $ct->company

View File

@ -46,7 +46,8 @@ class InvitationController extends Controller
Auth::logout(); Auth::logout();
$invitation = PurchaseOrderInvitation::where('key', $invitation_key) $invitation = PurchaseOrderInvitation::withTrashed()
->where('key', $invitation_key)
->whereHas('purchase_order', function ($query) { ->whereHas('purchase_order', function ($query) {
$query->where('is_deleted',0); $query->where('is_deleted',0);
}) })

View File

@ -141,6 +141,8 @@ class PurchaseOrderController extends Controller
->whereIn('id', $this->transformKeys($data['purchase_orders'])) ->whereIn('id', $this->transformKeys($data['purchase_orders']))
->where('company_id', auth()->guard('vendor')->user()->vendor->company_id) ->where('company_id', auth()->guard('vendor')->user()->vendor->company_id)
->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT]) ->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT])
->where('is_deleted', 0)
->withTrashed()
->cursor()->each(function ($purchase_order){ ->cursor()->each(function ($purchase_order){
$purchase_order->service() $purchase_order->service()
@ -159,7 +161,7 @@ class PurchaseOrderController extends Controller
if(count($data['purchase_orders']) == 1){ 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]); return redirect()->route('vendor.purchase_order.show', ['purchase_order' => $purchase_order->hashed_id]);

View File

@ -184,12 +184,49 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'gmail'; $this->mailer = 'gmail';
$this->setGmailMailer(); $this->setGmailMailer();
break; break;
case 'office365':
$this->mailer = 'office365';
$this->setOfficeMailer();
break;
default: default:
break; 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() private function setGmailMailer()
{ {
if(LaravelGmail::check()) 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;
}
} }

View File

@ -88,9 +88,15 @@ class TemplateEmail extends Mailable
$this->from(config('mail.from.address'), $email_from_name); $this->from(config('mail.from.address'), $email_from_name);
if (strlen($settings->bcc_email) > 1) 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->bcc(explode(",",str_replace(" ", "", $settings->bcc_email)));//remove whitespace if any has been inserted.
}
$this->subject($this->build_email->getSubject()) $this->subject($this->build_email->getSubject())
->text('email.template.text', [ ->text('email.template.text', [
'text_body' => $this->build_email->getTextBody(), 'text_body' => $this->build_email->getTextBody(),

View File

@ -374,6 +374,9 @@ class Account extends BaseModel
public function getDailyEmailLimit() public function getDailyEmailLimit()
{ {
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0)
return 50;
if($this->isPaid()){ if($this->isPaid()){
$limit = $this->paid_plan_email_quota; $limit = $this->paid_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100; $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;

View File

@ -39,7 +39,6 @@ class MailServiceProvider extends MailProvider
return new GmailTransportManager($app); return new GmailTransportManager($app);
}); });
//this is octane ready - but is untested //this is octane ready - but is untested
// $this->app->bind('mail.manager', function ($app){ // $this->app->bind('mail.manager', function ($app){
// return new GmailTransportManager($app); // return new GmailTransportManager($app);

View File

@ -18,6 +18,7 @@ use App\Models\CreditInvitation;
use App\Models\Document; use App\Models\Document;
use App\Transformers\ActivityTransformer; use App\Transformers\ActivityTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use League\Fractal\Resource\Item;
class CreditTransformer extends EntityTransformer class CreditTransformer extends EntityTransformer
{ {
@ -30,6 +31,7 @@ class CreditTransformer extends EntityTransformer
protected $availableIncludes = [ protected $availableIncludes = [
'activities', 'activities',
'client',
]; ];
public function includeActivities(Credit $credit) public function includeActivities(Credit $credit)
@ -53,28 +55,13 @@ class CreditTransformer extends EntityTransformer
return $this->includeCollection($credit->invitations, $transformer, CreditInvitation::class); return $this->includeCollection($credit->invitations, $transformer, CreditInvitation::class);
} }
/* public function includeClient(Credit $credit): Item
public function includePayments(quote $credit)
{ {
$transformer = new PaymentTransformer($this->account, $this->serializer, $credit); $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) public function includeDocuments(Credit $credit)
{ {
$transformer = new DocumentTransformer($this->serializer); $transformer = new DocumentTransformer($this->serializer);

View File

@ -71,6 +71,7 @@
"league/fractal": "^0.17.0", "league/fractal": "^0.17.0",
"league/omnipay": "^3.1", "league/omnipay": "^3.1",
"livewire/livewire": "^2.6", "livewire/livewire": "^2.6",
"microsoft/microsoft-graph": "^1.69",
"mollie/mollie-api-php": "^2.36", "mollie/mollie-api-php": "^2.36",
"nelexa/zip": "^4.0", "nelexa/zip": "^4.0",
"nwidart/laravel-modules": "8.3", "nwidart/laravel-modules": "8.3",

53
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "6845489fdc254427c4536e22f025ff51", "content-hash": "df84a1903809a8e781d937e679821e74",
"packages": [ "packages": [
{ {
"name": "afosto/yaac", "name": "afosto/yaac",
@ -4928,6 +4928,57 @@
], ],
"time": "2022-04-07T21:38:12+00:00" "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", "name": "mollie/mollie-api-php",
"version": "v2.44.1", "version": "v2.44.1",

View File

@ -73,6 +73,9 @@ return [
'gmail' => [ 'gmail' => [
'transport' => 'gmail', 'transport' => 'gmail',
], ],
'office365' => [
'transport' => 'office365',
],
'cocopostmark' => [ 'cocopostmark' => [
'transport' => 'cocopostmark', 'transport' => 'cocopostmark',
], ],

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.4.2', 'app_version' => '5.4.3',
'app_tag' => '5.4.2', 'app_tag' => '5.4.3',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),
@ -155,6 +155,11 @@ return [
'designs' => [ 'designs' => [
'base_path' => resource_path('views/pdf-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' => [ 'maintenance' => [
'delete_pdfs' => env('DELETE_PDF_DAYS', 0), 'delete_pdfs' => env('DELETE_PDF_DAYS', 0),
'delete_backups' => env('DELETE_BACKUP_DAYS', 0), 'delete_backups' => env('DELETE_BACKUP_DAYS', 0),

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeRefreshTokenColumnSize extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->text('oauth_user_refresh_token')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}