From 872ce3a3bc3e65c6cb5b4baf0dc8808cc6c7ca8a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 18:07:40 +1000 Subject: [PATCH 01/26] Fixes for event failure when viewing an invoice in the portal --- app/Listeners/Invoice/InvoiceViewedActivity.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Listeners/Invoice/InvoiceViewedActivity.php b/app/Listeners/Invoice/InvoiceViewedActivity.php index c3b0e52efe..b3afaf4032 100644 --- a/app/Listeners/Invoice/InvoiceViewedActivity.php +++ b/app/Listeners/Invoice/InvoiceViewedActivity.php @@ -46,10 +46,10 @@ class InvoiceViewedActivity implements ShouldQueue $fields = new \stdClass; - $fields->user_id = $event->invoice->user_id; - $fields->company_id = $event->invoice->company_id; + $fields->user_id = $event->invitation->user_id; + $fields->company_id = $event->invitation->company_id; $fields->activity_type_id = Activity::VIEW_INVOICE; - $fields->client_id = $event->invitation->client_id; + $fields->client_id = $event->invitation->invoice->client_id; $fields->client_contact_id = $event->invitation->client_contact_id; $fields->invitation_id = $event->invitation->id; $fields->invoice_id = $event->invitation->invoice_id; From 46bb38a20cdba99db3c369b57e47711c6c54354b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 18:28:29 +1000 Subject: [PATCH 02/26] Adjust client factories to allow contacts to be created by default --- app/Factory/ClientFactory.php | 4 ++-- app/Repositories/ClientContactRepository.php | 1 + app/Repositories/ClientRepository.php | 2 +- tests/Unit/FactoryCreationTest.php | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Factory/ClientFactory.php b/app/Factory/ClientFactory.php index 9c599ab6e1..7f6460ffb8 100644 --- a/app/Factory/ClientFactory.php +++ b/app/Factory/ClientFactory.php @@ -33,8 +33,8 @@ class ClientFactory $client->client_hash = Str::random(40); $client->settings = ClientSettings::defaults(); - $client_contact = ClientContactFactory::create($company_id, $user_id); - $client->contacts->add($client_contact); + // $client_contact = ClientContactFactory::create($company_id, $user_id); + // $client->contacts->add($client_contact); return $client; } diff --git a/app/Repositories/ClientContactRepository.php b/app/Repositories/ClientContactRepository.php index 0ef048db94..a363f513d4 100644 --- a/app/Repositories/ClientContactRepository.php +++ b/app/Repositories/ClientContactRepository.php @@ -30,6 +30,7 @@ class ClientContactRepository extends BaseRepository } else { $contacts = collect(); } +info(print_r($client->contacts->toArray(),1)); $client->contacts->pluck('id')->diff($contacts->pluck('id'))->each(function ($contact) { ClientContact::destroy($contact); diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php index 49d46d6920..8b5582cb71 100644 --- a/app/Repositories/ClientRepository.php +++ b/app/Repositories/ClientRepository.php @@ -78,7 +78,7 @@ class ClientRepository extends BaseRepository $data['name'] = $client->present()->name(); } - info("{$client->present()->name} has a balance of {$client->balance} with a paid to date of {$client->paid_to_date}"); + //info("{$client->present()->name} has a balance of {$client->balance} with a paid to date of {$client->paid_to_date}"); if (array_key_exists('documents', $data)) { $this->saveDocuments($data['documents'], $client); diff --git a/tests/Unit/FactoryCreationTest.php b/tests/Unit/FactoryCreationTest.php index 32eaf760b6..47c947da53 100644 --- a/tests/Unit/FactoryCreationTest.php +++ b/tests/Unit/FactoryCreationTest.php @@ -126,8 +126,8 @@ class FactoryCreationTest extends TestCase $cliz->save(); $this->assertNotNull($cliz->contacts); - $this->assertEquals(1, $cliz->contacts->count()); - $this->assertInternalType("int", $cliz->contacts->first()->id); + $this->assertEquals(0, $cliz->contacts->count()); + $this->assertInternalType("int", $cliz->id); } /** From 502bd4ad2d4210c6788fc590027948b681db2684 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 18:37:13 +1000 Subject: [PATCH 03/26] Fixes for test/demo data --- app/Console/Commands/CreateTestData.php | 1 + app/Repositories/ClientContactRepository.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/CreateTestData.php b/app/Console/Commands/CreateTestData.php index ac7bcf8daa..0045796945 100644 --- a/app/Console/Commands/CreateTestData.php +++ b/app/Console/Commands/CreateTestData.php @@ -286,6 +286,7 @@ class CreateTestData extends Command $company = factory(\App\Models\Company::class)->create([ 'account_id' => $account->id, 'slack_webhook_url' => config('ninja.notification.slack'), + 'is_large' => true, ]); $account->default_company_id = $company->id; diff --git a/app/Repositories/ClientContactRepository.php b/app/Repositories/ClientContactRepository.php index a363f513d4..0ef048db94 100644 --- a/app/Repositories/ClientContactRepository.php +++ b/app/Repositories/ClientContactRepository.php @@ -30,7 +30,6 @@ class ClientContactRepository extends BaseRepository } else { $contacts = collect(); } -info(print_r($client->contacts->toArray(),1)); $client->contacts->pluck('id')->diff($contacts->pluck('id'))->each(function ($contact) { ClientContact::destroy($contact); From f28a604d84cd02f9918352f23ad94c2122781b0a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 19:49:09 +1000 Subject: [PATCH 04/26] Fixes for invoicewasviewed --- app/Http/Controllers/ClientPortal/InvitationController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 8453e788f7..533c419e06 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -54,7 +54,7 @@ class InvitationController extends Controller event(new InvitationWasViewed($invitation->{$entity}, $invitation, $invitation->{$entity}->company, Ninja::eventVars())); - $this->fireEntityViewedEvent($invitation->{$entity}, $entity); + $this->fireEntityViewedEvent($invitation, $entity); } return redirect()->route('client.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->{$key})]); From d5b777206e884fd7fcd88e8966daf7167c971e85 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 21:19:51 +1000 Subject: [PATCH 05/26] Shop routes --- .../Controllers/Shop/InvoiceController.php | 83 +++++++++++++++++++ .../Controllers/Shop/ProductController.php | 53 ++++++++++++ app/Http/Kernel.php | 7 ++ app/Http/Middleware/Shop/ShopTokenAuth.php | 78 +++++++++++++++++ app/Http/Middleware/TokenAuth.php | 14 ++++ app/Models/CompanyUser.php | 1 + app/Providers/RouteServiceProvider.php | 10 +++ .../2020_07_28_104218_shop_token.php | 30 +++++++ routes/shop.php | 14 ++++ 9 files changed, 290 insertions(+) create mode 100644 app/Http/Controllers/Shop/InvoiceController.php create mode 100644 app/Http/Controllers/Shop/ProductController.php create mode 100644 app/Http/Middleware/Shop/ShopTokenAuth.php create mode 100644 database/migrations/2020_07_28_104218_shop_token.php create mode 100644 routes/shop.php diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php new file mode 100644 index 0000000000..b0723ae6fb --- /dev/null +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -0,0 +1,83 @@ +invoice_repo = $invoice_repo; + } + + public function show(string $invitation_key) + { + $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + + $invitation = InvoiceInvitation::with(['invoice']) + ->where('company_id', $company_token->company->id) + ->where('key',$invitation_key) + ->firstOrFail(); + + return $this->itemResponse($invitation->invoice); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function store(StoreInvoiceRequest $request) + { + $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + + $client = Client::find($request->input('client_id')); + + $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company_token->company_id, $company_token->user_id)); + + event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars())); + + $invoice = $invoice->service()->triggeredActions($request)->save(); + + return $this->itemResponse($invoice); + } + +} diff --git a/app/Http/Controllers/Shop/ProductController.php b/app/Http/Controllers/Shop/ProductController.php new file mode 100644 index 0000000000..c68131d514 --- /dev/null +++ b/app/Http/Controllers/Shop/ProductController.php @@ -0,0 +1,53 @@ +whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + + $products = Product::where('company_id', $company_token->company->id); + + return $this->listResponse($products); + } + + public function show(string $product_key) + { + $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + + $product = Product::where('company_id', $company_token->company->id) + ->where('product_key', $product_key) + ->first(); + + return $this->itemResponse($product); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 4799bf0fc7..9ec5e5972e 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -73,6 +73,11 @@ class Kernel extends HttpKernel \App\Http\Middleware\StartupCheck::class, \App\Http\Middleware\QueryLogging::class, ], + 'shop' => [ + 'throttle:60,1', + 'bindings', + 'query_logging', + ], ]; /** @@ -108,5 +113,7 @@ class Kernel extends HttpKernel 'api_db' => \App\Http\Middleware\SetDb::class, 'locale' => \App\Http\Middleware\Locale::class, 'contact.register' => \App\Http\Middleware\ContactRegister::class, + 'shop_token_auth' => \App\Http\Middleware\ShopTokenAuth::class, + ]; } diff --git a/app/Http/Middleware/Shop/ShopTokenAuth.php b/app/Http/Middleware/Shop/ShopTokenAuth.php new file mode 100644 index 0000000000..2af0eb0df4 --- /dev/null +++ b/app/Http/Middleware/Shop/ShopTokenAuth.php @@ -0,0 +1,78 @@ +header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user','company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first())) { + + /* Check if this is a restricted token*/ + if(!$company_token->shop_restricted){ + + $error = [ + 'message' => 'Cannot use a unrestricted token on this route', + 'errors' => [] + ]; + + + return response()->json($error, 403); + + } + + $user = $company_token->user; + + $error = [ + 'message' => 'User inactive', + 'errors' => [] + ]; + + //user who once existed, but has been soft deleted + if (!$user) { + return response()->json($error, 403); + } + + /* + | + | Necessary evil here: As we are authenticating on CompanyToken, + | we need to link the company to the user manually. This allows + | us to decouple a $user and their attached companies completely. + | + */ + $user->setCompany($company_token->company); + + config(['ninja.company_id' => $company_token->company->id]); + + app('queue')->createPayloadUsing(function () use ($company_token) { + return ['db' => $company_token->company->db]; + }); + + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index b3f840fa6e..9b4ebda3e4 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -29,6 +29,20 @@ class TokenAuth public function handle($request, Closure $next) { if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user','company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first())) { + + if($company_token->shop_restricted){ + + $error = [ + 'message' => 'Cannot use a restricted token on this route', + 'errors' => [] + ]; + + + return response()->json($error, 403); + + } + + $user = $company_token->user; $error = [ diff --git a/app/Models/CompanyUser.php b/app/Models/CompanyUser.php index 5ce07b3efd..fa699b40e2 100644 --- a/app/Models/CompanyUser.php +++ b/app/Models/CompanyUser.php @@ -46,6 +46,7 @@ class CompanyUser extends Pivot 'is_owner', 'is_locked', 'slack_webhook_url', + 'shop_restricted' ]; protected $touches = []; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 778e1d5ff0..feddf0b08b 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -57,6 +57,8 @@ class RouteServiceProvider extends ServiceProvider $this->mapContactApiRoutes(); $this->mapClientApiRoutes(); + + $this->mapShopApiRoutes(); } /** @@ -117,4 +119,12 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace) ->group(base_path('routes/client.php')); } + + protected function mapShopApiRoutes() + { + Route::prefix('') + ->middleware('shop') + ->namespace($this->namespace) + ->group(base_path('routes/shop.php')); + } } diff --git a/database/migrations/2020_07_28_104218_shop_token.php b/database/migrations/2020_07_28_104218_shop_token.php new file mode 100644 index 0000000000..49dadf527b --- /dev/null +++ b/database/migrations/2020_07_28_104218_shop_token.php @@ -0,0 +1,30 @@ +boolean('shop_restricted')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/routes/shop.php b/routes/shop.php new file mode 100644 index 0000000000..783a503a46 --- /dev/null +++ b/routes/shop.php @@ -0,0 +1,14 @@ + ['api_db','shop_token_auth','locale']], function () { + + Route::get('products', 'Shop\ProductController@index'); + Route::get('clients', 'Shop\ClientController@index'); + Route::get('invoices', 'Shop\InvoiceController@index'); + Route::get('client/{contact_key}', 'Shop\ClientController@show'); + Route::get('invoice/{invitation_key}', 'Shop\InvoiceController@show'); + Route::get('product/{product_key}', 'Shop\ProductController@show'); + +}); \ No newline at end of file From a8a0c7695c5c6a5dde15e2e229308924e98be6cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 21:30:11 +1000 Subject: [PATCH 06/26] Shop routes --- .../Controllers/Shop/ClientController.php | 77 +++++++++++++++++++ .../Controllers/Shop/InvoiceController.php | 8 +- routes/api.php | 4 +- 3 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 app/Http/Controllers/Shop/ClientController.php diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php new file mode 100644 index 0000000000..2fec7c5d2f --- /dev/null +++ b/app/Http/Controllers/Shop/ClientController.php @@ -0,0 +1,77 @@ +client_repo = $client_repo; + } + + public function show(string $contact_key) + { + $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + + $contact = ClientContact::with('client') + ->where('company_id', $company_token->company->id) + ->where('contact_key', $contact_key) + ->firstOrFail(); + + return $this->itemResponse($contact->client); + } + + public function store(StoreClientRequest $request) + { + $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + + $client = $this->client_repo->save($request->all(), ClientFactory::create($company_token->company_id, $company_token->user_id)); + + $client->load('contacts', 'primary_contact'); + + $this->uploadLogo($request->file('company_logo'), $client->company, $client); + + event(new ClientWasCreated($client, $client->company, Ninja::eventVars())); + + return $this->itemResponse($client); + } +} diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php index b0723ae6fb..97b5a3e25e 100644 --- a/app/Http/Controllers/Shop/InvoiceController.php +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -11,6 +11,7 @@ namespace App\Http\Controllers\Shop; +use App\Events\Invoice\InvoiceWasCreated; use App\Factory\InvoiceFactory; use App\Http\Controllers\BaseController; use App\Http\Requests\Invoice\StoreInvoiceRequest; @@ -20,6 +21,7 @@ use App\Models\Invoice; use App\Models\InvoiceInvitation; use App\Repositories\InvoiceRepository; use App\Transformers\InvoiceTransformer; +use App\Utils\Ninja; use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; @@ -60,11 +62,7 @@ class InvoiceController extends BaseController return $this->itemResponse($invitation->invoice); } - /** - * Display a listing of the resource. - * - * @return \Illuminate\Http\Response - */ + public function store(StoreInvoiceRequest $request) { $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); diff --git a/routes/api.php b/routes/api.php index e1feb2bdee..1db2f96a6b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -136,8 +136,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('emails', 'EmailController@send')->name('email.send'); /*Subscription and Webhook routes */ - Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); - Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); + // Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); + // Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); Route::resource('webhooks', 'WebhookController'); Route::post('webhooks/bulk', 'WebhookController@bulk')->name('webhooks.bulk'); From caad3661d52c1337d9a65a6c259e92f2b1f41f6b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 21:58:15 +1000 Subject: [PATCH 07/26] Refactor to user company_key instead of company_token --- app/Http/Controllers/BaseController.php | 2 +- .../Controllers/Shop/ClientController.php | 16 ++-- .../Controllers/Shop/InvoiceController.php | 14 ++-- .../Controllers/Shop/ProductController.php | 13 ++-- app/Http/Kernel.php | 3 +- app/Http/Middleware/SetDbByCompanyKey.php | 48 ++++++++++++ app/Http/Middleware/Shop/ShopTokenAuth.php | 78 ------------------- app/Http/Middleware/TokenAuth.php | 13 ---- app/Libraries/MultiDB.php | 11 +++ .../2020_07_28_104218_shop_token.php | 4 +- routes/shop.php | 14 ++-- 11 files changed, 97 insertions(+), 119 deletions(-) create mode 100644 app/Http/Middleware/SetDbByCompanyKey.php delete mode 100644 app/Http/Middleware/Shop/ShopTokenAuth.php diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index e741f1466d..5033780abb 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -270,7 +270,7 @@ class BaseController extends Controller $query->with($includes); - if (!auth()->user()->hasPermission('view_'.lcfirst(class_basename($this->entity_type)))) { + if (auth()->user() && !auth()->user()->hasPermission('view_'.lcfirst(class_basename($this->entity_type)))) { $query->where('user_id', '=', auth()->user()->id); } diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php index 2fec7c5d2f..7219e857e7 100644 --- a/app/Http/Controllers/Shop/ClientController.php +++ b/app/Http/Controllers/Shop/ClientController.php @@ -50,10 +50,10 @@ class ClientController extends BaseController public function show(string $contact_key) { - $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); $contact = ClientContact::with('client') - ->where('company_id', $company_token->company->id) + ->where('company_id', $company->id) ->where('contact_key', $contact_key) ->firstOrFail(); @@ -62,15 +62,19 @@ class ClientController extends BaseController public function store(StoreClientRequest $request) { - $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); - $client = $this->client_repo->save($request->all(), ClientFactory::create($company_token->company_id, $company_token->user_id)); + app('queue')->createPayloadUsing(function () use ($company) { + return ['db' => $company->db]; + }); + + $client = $this->client_repo->save($request->all(), ClientFactory::create($company->id, $company->owner()->id)); $client->load('contacts', 'primary_contact'); - $this->uploadLogo($request->file('company_logo'), $client->company, $client); + $this->uploadLogo($request->file('company_logo'), $company, $client); - event(new ClientWasCreated($client, $client->company, Ninja::eventVars())); + event(new ClientWasCreated($client, $company, Ninja::eventVars())); return $this->itemResponse($client); } diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php index 97b5a3e25e..198613dc51 100644 --- a/app/Http/Controllers/Shop/InvoiceController.php +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -52,10 +52,10 @@ class InvoiceController extends BaseController public function show(string $invitation_key) { - $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); $invitation = InvoiceInvitation::with(['invoice']) - ->where('company_id', $company_token->company->id) + ->where('company_id', $company->id) ->where('key',$invitation_key) ->firstOrFail(); @@ -65,13 +65,17 @@ class InvoiceController extends BaseController public function store(StoreInvoiceRequest $request) { - $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + app('queue')->createPayloadUsing(function () use ($company) { + return ['db' => $company->db]; + }); + + $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); $client = Client::find($request->input('client_id')); - $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company_token->company_id, $company_token->user_id)); + $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company_id, $company->owner()->id)); - event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars())); + event(new InvoiceWasCreated($invoice, $company, Ninja::eventVars())); $invoice = $invoice->service()->triggeredActions($request)->save(); diff --git a/app/Http/Controllers/Shop/ProductController.php b/app/Http/Controllers/Shop/ProductController.php index c68131d514..5122ba1918 100644 --- a/app/Http/Controllers/Shop/ProductController.php +++ b/app/Http/Controllers/Shop/ProductController.php @@ -12,6 +12,7 @@ namespace App\Http\Controllers\Shop; use App\Http\Controllers\BaseController; +use App\Models\Company; use App\Models\CompanyToken; use App\Models\Product; use App\Transformers\ProductTransformer; @@ -31,20 +32,20 @@ class ProductController extends BaseController * * @return \Illuminate\Http\Response */ - public function index() + public function index(Request $request) { - $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); - $products = Product::where('company_id', $company_token->company->id); + $products = Product::where('company_id', $company->id); return $this->listResponse($products); } - public function show(string $product_key) + public function show(Request $request, string $product_key) { - $company_token = CompanyToken::with(['company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); - $product = Product::where('company_id', $company_token->company->id) + $product = Product::where('company_id', $company->id) ->where('product_key', $product_key) ->first(); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 9ec5e5972e..ca3680122a 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -111,9 +111,10 @@ class Kernel extends HttpKernel 'url_db' => \App\Http\Middleware\UrlSetDb::class, 'web_db' => \App\Http\Middleware\SetWebDb::class, 'api_db' => \App\Http\Middleware\SetDb::class, + 'company_key_db' => \App\Http\Middleware\SetDbByCompanyKey::class, 'locale' => \App\Http\Middleware\Locale::class, 'contact.register' => \App\Http\Middleware\ContactRegister::class, - 'shop_token_auth' => \App\Http\Middleware\ShopTokenAuth::class, + 'shop_token_auth' => \App\Http\Middleware\Shop\ShopTokenAuth::class, ]; } diff --git a/app/Http/Middleware/SetDbByCompanyKey.php b/app/Http/Middleware/SetDbByCompanyKey.php new file mode 100644 index 0000000000..74c9951272 --- /dev/null +++ b/app/Http/Middleware/SetDbByCompanyKey.php @@ -0,0 +1,48 @@ + 'Invalid Token', + 'errors' => [] + ]; + + + if ($request->header('X-API-COMPANY_KEY') && config('ninja.db.multi_db_enabled')) { + if (! MultiDB::findAndSetDbByCompanyKey($request->header('X-API-COMPANY_KEY'))) { + return response()->json($error, 403); + } + } elseif (!config('ninja.db.multi_db_enabled')) { + return $next($request); + } else { + return response()->json($error, 403); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Shop/ShopTokenAuth.php b/app/Http/Middleware/Shop/ShopTokenAuth.php deleted file mode 100644 index 2af0eb0df4..0000000000 --- a/app/Http/Middleware/Shop/ShopTokenAuth.php +++ /dev/null @@ -1,78 +0,0 @@ -header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user','company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first())) { - - /* Check if this is a restricted token*/ - if(!$company_token->shop_restricted){ - - $error = [ - 'message' => 'Cannot use a unrestricted token on this route', - 'errors' => [] - ]; - - - return response()->json($error, 403); - - } - - $user = $company_token->user; - - $error = [ - 'message' => 'User inactive', - 'errors' => [] - ]; - - //user who once existed, but has been soft deleted - if (!$user) { - return response()->json($error, 403); - } - - /* - | - | Necessary evil here: As we are authenticating on CompanyToken, - | we need to link the company to the user manually. This allows - | us to decouple a $user and their attached companies completely. - | - */ - $user->setCompany($company_token->company); - - config(['ninja.company_id' => $company_token->company->id]); - - app('queue')->createPayloadUsing(function () use ($company_token) { - return ['db' => $company_token->company->db]; - }); - - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index 9b4ebda3e4..088e51cd43 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -30,19 +30,6 @@ class TokenAuth { if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user','company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first())) { - if($company_token->shop_restricted){ - - $error = [ - 'message' => 'Cannot use a restricted token on this route', - 'errors' => [] - ]; - - - return response()->json($error, 403); - - } - - $user = $company_token->user; $error = [ diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 457eee5070..d15196a40b 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -180,6 +180,17 @@ class MultiDB return false; } + public static function findAndSetDbByCompanyKey($company_key) :bool + { + foreach (self::$dbs as $db) { + if ($company = Company::on($db)->where('company_key', $company_key)->first()) { + self::setDb($company->db); + return true; + } + } + return false; + } + public static function findAndSetDbByDomain($subdomain) :bool { foreach (self::$dbs as $db) { diff --git a/database/migrations/2020_07_28_104218_shop_token.php b/database/migrations/2020_07_28_104218_shop_token.php index 49dadf527b..7996ffa0a8 100644 --- a/database/migrations/2020_07_28_104218_shop_token.php +++ b/database/migrations/2020_07_28_104218_shop_token.php @@ -13,8 +13,8 @@ class ShopToken extends Migration */ public function up() { - Schema::table('company_user', function (Blueprint $table) { - $table->boolean('shop_restricted')->default(false); + Schema::table('companies', function (Blueprint $table) { + $table->boolean('enable_shop_api')->default(false); }); } diff --git a/routes/shop.php b/routes/shop.php index 783a503a46..aee2d2f756 100644 --- a/routes/shop.php +++ b/routes/shop.php @@ -2,13 +2,13 @@ use Illuminate\Support\Facades\Route; -Route::group(['middleware' => ['api_db','shop_token_auth','locale']], function () { +Route::group(['middleware' => ['company_key_db','locale'], 'prefix' => 'api/v1'], function () { - Route::get('products', 'Shop\ProductController@index'); - Route::get('clients', 'Shop\ClientController@index'); - Route::get('invoices', 'Shop\InvoiceController@index'); - Route::get('client/{contact_key}', 'Shop\ClientController@show'); - Route::get('invoice/{invitation_key}', 'Shop\InvoiceController@show'); - Route::get('product/{product_key}', 'Shop\ProductController@show'); + Route::get('shop/products', 'Shop\ProductController@index'); + Route::get('shop/clients', 'Shop\ClientController@index'); + Route::get('shop/invoices', 'Shop\InvoiceController@index'); + Route::get('shop/client/{contact_key}', 'Shop\ClientController@show'); + Route::get('shop/invoice/{invitation_key}', 'Shop\InvoiceController@show'); + Route::get('shop/product/{product_key}', 'Shop\ProductController@show'); }); \ No newline at end of file From de78ea1506451693240f069b61a727111d39c06b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 22:05:17 +1000 Subject: [PATCH 08/26] Shop --- .../Controllers/Shop/ClientController.php | 3 +- tests/Feature/Shop/ShopInvoiceTest.php | 101 ++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Shop/ShopInvoiceTest.php diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php index 7219e857e7..31002e57b4 100644 --- a/app/Http/Controllers/Shop/ClientController.php +++ b/app/Http/Controllers/Shop/ClientController.php @@ -17,6 +17,7 @@ use App\Http\Controllers\BaseController; use App\Http\Requests\Client\StoreClientRequest; use App\Models\Client; use App\Models\ClientContact; +use App\Models\Company; use App\Models\CompanyToken; use App\Repositories\ClientRepository; use App\Transformers\ClientTransformer; @@ -48,7 +49,7 @@ class ClientController extends BaseController $this->client_repo = $client_repo; } - public function show(string $contact_key) + public function show(Request $request, string $contact_key) { $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); diff --git a/tests/Feature/Shop/ShopInvoiceTest.php b/tests/Feature/Shop/ShopInvoiceTest.php new file mode 100644 index 0000000000..26657dc6be --- /dev/null +++ b/tests/Feature/Shop/ShopInvoiceTest.php @@ -0,0 +1,101 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testTokenFailure() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY_KEY' => $this->company->company_key + ])->get('/api/v1/shop/products'); + + + $response->assertStatus(200); + } + + public function testTokenSuccess() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY_KEY' => $this->company->company_key + ])->get('/api/v1/products'); + + + $response->assertStatus(403); + + $arr = $response->json(); + } + + public function testGetByProductKey() + { + $product = factory(\App\Models\Product::class)->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY_KEY' => $this->company->company_key + ])->get('/api/v1/shop/product/'.$product->product_key); + + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals($product->hashed_id, $arr['data']['id']); + } + + public function testGetByClientByContactKey() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY_KEY' => $this->company->company_key + ])->get('/api/v1/shop/client/'.$this->client->contacts->first()->contact_key); + + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals($this->client->hashed_id, $arr['data']['id']); + + } +} From 2b9610ea022eb51d171de5d3fbfc4f17701d076e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 22:06:47 +1000 Subject: [PATCH 09/26] clean up for basecontroller --- app/Http/Controllers/BaseController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 5033780abb..ccf9ee51c4 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -346,7 +346,7 @@ class BaseController extends Controller $data = $this->createItem($item, $transformer, $this->entity_type); - if (request()->include_static) { + if (auth()->user() && request()->include_static) { $data['static'] = Statics::company(auth()->user()->getCompany()->getLocale()); } From 52065fb9633e85f4edcf4e6b5f5c7ffaa3fc0ee4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 22:12:33 +1000 Subject: [PATCH 10/26] check shop is enabled --- app/Http/Controllers/Shop/ClientController.php | 6 ++++++ app/Http/Controllers/Shop/InvoiceController.php | 13 ++++++++++--- app/Http/Controllers/Shop/ProductController.php | 6 ++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php index 31002e57b4..52800c568b 100644 --- a/app/Http/Controllers/Shop/ClientController.php +++ b/app/Http/Controllers/Shop/ClientController.php @@ -53,6 +53,9 @@ class ClientController extends BaseController { $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + $contact = ClientContact::with('client') ->where('company_id', $company->id) ->where('contact_key', $contact_key) @@ -65,6 +68,9 @@ class ClientController extends BaseController { $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + app('queue')->createPayloadUsing(function () use ($company) { return ['db' => $company->db]; }); diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php index 198613dc51..ace6fef91a 100644 --- a/app/Http/Controllers/Shop/InvoiceController.php +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -54,6 +54,9 @@ class InvoiceController extends BaseController { $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + $invitation = InvoiceInvitation::with(['invoice']) ->where('company_id', $company->id) ->where('key',$invitation_key) @@ -65,12 +68,16 @@ class InvoiceController extends BaseController public function store(StoreInvoiceRequest $request) { - app('queue')->createPayloadUsing(function () use ($company) { - return ['db' => $company->db]; - }); $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + + app('queue')->createPayloadUsing(function () use ($company) { + return ['db' => $company->db]; + }); + $client = Client::find($request->input('client_id')); $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company_id, $company->owner()->id)); diff --git a/app/Http/Controllers/Shop/ProductController.php b/app/Http/Controllers/Shop/ProductController.php index 5122ba1918..77ad205307 100644 --- a/app/Http/Controllers/Shop/ProductController.php +++ b/app/Http/Controllers/Shop/ProductController.php @@ -36,6 +36,9 @@ class ProductController extends BaseController { $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + $products = Product::where('company_id', $company->id); return $this->listResponse($products); @@ -45,6 +48,9 @@ class ProductController extends BaseController { $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + if(!$company->enable_shop_api) + return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); + $product = Product::where('company_id', $company->id) ->where('product_key', $product_key) ->first(); From ffe73b77af2ac9e3d4f71b041c3b091b19645e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Jul 2020 14:41:10 +0200 Subject: [PATCH 11/26] Undo setup changes --- app/Utils/SystemHealth.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index b752c37764..bc4fbcd160 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -90,13 +90,14 @@ class SystemHealth exec('node -v', $foo, $exitCode); if ($exitCode === 0) { - return true; + return $foo[0]; } - - return false; + } catch (\Exception $e) { - return false; + + return false; } + } public static function checkNpm() @@ -105,14 +106,14 @@ class SystemHealth exec('npm -v', $foo, $exitCode); if ($exitCode === 0) { - return true; - } + return $foo[0]; + } - return false; - - } catch (\Exception $e) { - return false; + }catch (\Exception $e) { + + return false; } + } private static function simpleDbCheck() :bool From d91459b48f8d1e9f1ed2620a0389732cd9042d9f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 22:41:24 +1000 Subject: [PATCH 12/26] Set demo for company_key on demo account --- app/Console/Commands/DemoMode.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Console/Commands/DemoMode.php b/app/Console/Commands/DemoMode.php index a2cb0781c4..ba89a7934d 100644 --- a/app/Console/Commands/DemoMode.php +++ b/app/Console/Commands/DemoMode.php @@ -101,6 +101,7 @@ class DemoMode extends Command 'account_id' => $account->id, 'slack_webhook_url' => config('ninja.notification.slack'), 'enabled_modules' => 32767, + 'company_key' => 'demo', ]); $settings = $company->settings; From b28a48b1d9edf5b5a6d46fb7c060fba878aed2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 28 Jul 2020 14:42:53 +0200 Subject: [PATCH 13/26] undo --- app/Utils/SystemHealth.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index bc4fbcd160..6e7eb880b6 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -60,14 +60,6 @@ class SystemHealth $system_health = false; } - if (!self::checkNode()) { - $system_health = false; - } - - if (!self::checkNpm()) { - $system_health = false; - } - return [ 'system_health' => $system_health, 'extensions' => self::extensions(), From 44894edb17297e0f3b6fe8ccdc91ee18dcb5783d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 22:47:44 +1000 Subject: [PATCH 14/26] Enable shop api on demo account --- app/Console/Commands/DemoMode.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Console/Commands/DemoMode.php b/app/Console/Commands/DemoMode.php index ba89a7934d..92decce9a6 100644 --- a/app/Console/Commands/DemoMode.php +++ b/app/Console/Commands/DemoMode.php @@ -102,6 +102,7 @@ class DemoMode extends Command 'slack_webhook_url' => config('ninja.notification.slack'), 'enabled_modules' => 32767, 'company_key' => 'demo', + 'enable_shop_api' => true ]); $settings = $company->settings; From fb34f2654aba03b491672e73408caa4edfaba2e0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 22:55:40 +1000 Subject: [PATCH 15/26] Allow additional headers --- app/Http/Middleware/Cors.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Middleware/Cors.php b/app/Http/Middleware/Cors.php index 137532e8d8..56ff06fcb3 100644 --- a/app/Http/Middleware/Cors.php +++ b/app/Http/Middleware/Cors.php @@ -16,7 +16,7 @@ class Cors // ALLOW OPTIONS METHOD $headers = [ 'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE', - 'Access-Control-Allow-Headers'=> 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' + 'Access-Control-Allow-Headers'=> 'X-API-COMPANY_KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' ]; return Response::make('OK', 200, $headers); @@ -36,7 +36,7 @@ class Cors $response->headers->set('Access-Control-Allow-Origin', '*'); $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - $response->headers->set('Access-Control-Allow-Headers', 'X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); + $response->headers->set('Access-Control-Allow-Headers', 'X-API-COMPANY_KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); $response->headers->set('Access-Control-Expose-Headers', 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION'); $response->headers->set('X-APP-VERSION', config('ninja.app_version')); $response->headers->set('X-MINIMUM-CLIENT-VERSION', config('ninja.minimum_client_version')); From fd97aff3f14f424dbc4f075e8d455d6fa7d0a95c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 23:24:01 +1000 Subject: [PATCH 16/26] Fixes for routes" git push --- app/Http/Controllers/Shop/ClientController.php | 4 ++-- app/Http/Controllers/Shop/InvoiceController.php | 4 ++-- app/Http/Controllers/Shop/ProductController.php | 4 ++-- app/Http/Middleware/Cors.php | 4 ++-- app/Http/Middleware/SetDbByCompanyKey.php | 4 ++-- routes/shop.php | 4 ++-- tests/Feature/Shop/ShopInvoiceTest.php | 8 ++++---- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php index 52800c568b..1a52b35e63 100644 --- a/app/Http/Controllers/Shop/ClientController.php +++ b/app/Http/Controllers/Shop/ClientController.php @@ -51,7 +51,7 @@ class ClientController extends BaseController public function show(Request $request, string $contact_key) { - $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); if(!$company->enable_shop_api) return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); @@ -66,7 +66,7 @@ class ClientController extends BaseController public function store(StoreClientRequest $request) { - $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); if(!$company->enable_shop_api) return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php index ace6fef91a..003234f13e 100644 --- a/app/Http/Controllers/Shop/InvoiceController.php +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -52,7 +52,7 @@ class InvoiceController extends BaseController public function show(string $invitation_key) { - $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); if(!$company->enable_shop_api) return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); @@ -69,7 +69,7 @@ class InvoiceController extends BaseController public function store(StoreInvoiceRequest $request) { - $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); if(!$company->enable_shop_api) return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); diff --git a/app/Http/Controllers/Shop/ProductController.php b/app/Http/Controllers/Shop/ProductController.php index 77ad205307..9489976d6f 100644 --- a/app/Http/Controllers/Shop/ProductController.php +++ b/app/Http/Controllers/Shop/ProductController.php @@ -34,7 +34,7 @@ class ProductController extends BaseController */ public function index(Request $request) { - $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); if(!$company->enable_shop_api) return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); @@ -46,7 +46,7 @@ class ProductController extends BaseController public function show(Request $request, string $product_key) { - $company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first(); + $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); if(!$company->enable_shop_api) return response()->json(['message' => 'Shop is disabled', 'errors' => []],403); diff --git a/app/Http/Middleware/Cors.php b/app/Http/Middleware/Cors.php index 56ff06fcb3..09586788e6 100644 --- a/app/Http/Middleware/Cors.php +++ b/app/Http/Middleware/Cors.php @@ -16,7 +16,7 @@ class Cors // ALLOW OPTIONS METHOD $headers = [ 'Access-Control-Allow-Methods'=> 'POST, GET, OPTIONS, PUT, DELETE', - 'Access-Control-Allow-Headers'=> 'X-API-COMPANY_KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' + 'Access-Control-Allow-Headers'=> 'X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' ]; return Response::make('OK', 200, $headers); @@ -36,7 +36,7 @@ class Cors $response->headers->set('Access-Control-Allow-Origin', '*'); $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - $response->headers->set('Access-Control-Allow-Headers', 'X-API-COMPANY_KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); + $response->headers->set('Access-Control-Allow-Headers', 'X-API-COMPANY-KEY,X-API-SECRET,X-API-TOKEN,X-API-PASSWORD,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'); $response->headers->set('Access-Control-Expose-Headers', 'X-APP-VERSION,X-MINIMUM-CLIENT-VERSION'); $response->headers->set('X-APP-VERSION', config('ninja.app_version')); $response->headers->set('X-MINIMUM-CLIENT-VERSION', config('ninja.minimum_client_version')); diff --git a/app/Http/Middleware/SetDbByCompanyKey.php b/app/Http/Middleware/SetDbByCompanyKey.php index 74c9951272..e2abfe9bc4 100644 --- a/app/Http/Middleware/SetDbByCompanyKey.php +++ b/app/Http/Middleware/SetDbByCompanyKey.php @@ -33,8 +33,8 @@ class SetDbByCompanyKey ]; - if ($request->header('X-API-COMPANY_KEY') && config('ninja.db.multi_db_enabled')) { - if (! MultiDB::findAndSetDbByCompanyKey($request->header('X-API-COMPANY_KEY'))) { + if ($request->header('X-API-COMPANY-KEY') && config('ninja.db.multi_db_enabled')) { + if (! MultiDB::findAndSetDbByCompanyKey($request->header('X-API-COMPANY-KEY'))) { return response()->json($error, 403); } } elseif (!config('ninja.db.multi_db_enabled')) { diff --git a/routes/shop.php b/routes/shop.php index aee2d2f756..9187fdf2be 100644 --- a/routes/shop.php +++ b/routes/shop.php @@ -5,8 +5,8 @@ use Illuminate\Support\Facades\Route; Route::group(['middleware' => ['company_key_db','locale'], 'prefix' => 'api/v1'], function () { Route::get('shop/products', 'Shop\ProductController@index'); - Route::get('shop/clients', 'Shop\ClientController@index'); - Route::get('shop/invoices', 'Shop\InvoiceController@index'); + Route::post('shop/clients', 'Shop\ClientController@store'); + Route::post('shop/invoices', 'Shop\InvoiceController@store'); Route::get('shop/client/{contact_key}', 'Shop\ClientController@show'); Route::get('shop/invoice/{invitation_key}', 'Shop\InvoiceController@show'); Route::get('shop/product/{product_key}', 'Shop\ProductController@show'); diff --git a/tests/Feature/Shop/ShopInvoiceTest.php b/tests/Feature/Shop/ShopInvoiceTest.php index 26657dc6be..e921185ff1 100644 --- a/tests/Feature/Shop/ShopInvoiceTest.php +++ b/tests/Feature/Shop/ShopInvoiceTest.php @@ -42,7 +42,7 @@ class ShopInvoiceTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-COMPANY_KEY' => $this->company->company_key + 'X-API-COMPANY-KEY' => $this->company->company_key ])->get('/api/v1/shop/products'); @@ -54,7 +54,7 @@ class ShopInvoiceTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-COMPANY_KEY' => $this->company->company_key + 'X-API-COMPANY-KEY' => $this->company->company_key ])->get('/api/v1/products'); @@ -72,7 +72,7 @@ class ShopInvoiceTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-COMPANY_KEY' => $this->company->company_key + 'X-API-COMPANY-KEY' => $this->company->company_key ])->get('/api/v1/shop/product/'.$product->product_key); @@ -88,7 +88,7 @@ class ShopInvoiceTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-COMPANY_KEY' => $this->company->company_key + 'X-API-COMPANY-KEY' => $this->company->company_key ])->get('/api/v1/shop/client/'.$this->client->contacts->first()->contact_key); From 8f5951595073492cb45cf30baa8557c8c66763a3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 23:41:56 +1000 Subject: [PATCH 17/26] Fixes for shop post routes --- .../Controllers/Shop/ClientController.php | 7 +- .../Controllers/Shop/InvoiceController.php | 6 +- .../Requests/Shop/StoreShopClientRequest.php | 183 ++++++++++++++++++ .../Requests/Shop/StoreShopInvoiceRequest.php | 109 +++++++++++ 4 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 app/Http/Requests/Shop/StoreShopClientRequest.php create mode 100644 app/Http/Requests/Shop/StoreShopInvoiceRequest.php diff --git a/app/Http/Controllers/Shop/ClientController.php b/app/Http/Controllers/Shop/ClientController.php index 1a52b35e63..755615a9e5 100644 --- a/app/Http/Controllers/Shop/ClientController.php +++ b/app/Http/Controllers/Shop/ClientController.php @@ -15,6 +15,7 @@ use App\Events\Client\ClientWasCreated; use App\Factory\ClientFactory; use App\Http\Controllers\BaseController; use App\Http\Requests\Client\StoreClientRequest; +use App\Http\Requests\Shop\StoreShopClientRequest; use App\Models\Client; use App\Models\ClientContact; use App\Models\Company; @@ -23,12 +24,14 @@ use App\Repositories\ClientRepository; use App\Transformers\ClientTransformer; use App\Utils\Ninja; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\Uploadable; use Illuminate\Http\Request; class ClientController extends BaseController { use MakesHash; - + use Uploadable; + protected $entity_type = Client::class; protected $entity_transformer = ClientTransformer::class; @@ -64,7 +67,7 @@ class ClientController extends BaseController return $this->itemResponse($contact->client); } - public function store(StoreClientRequest $request) + public function store(StoreShopClientRequest $request) { $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php index 003234f13e..3a70c75a73 100644 --- a/app/Http/Controllers/Shop/InvoiceController.php +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -15,7 +15,9 @@ use App\Events\Invoice\InvoiceWasCreated; use App\Factory\InvoiceFactory; use App\Http\Controllers\BaseController; use App\Http\Requests\Invoice\StoreInvoiceRequest; +use App\Http\Requests\Shop\StoreShopInvoiceRequest; use App\Models\Client; +use App\Models\Company; use App\Models\CompanyToken; use App\Models\Invoice; use App\Models\InvoiceInvitation; @@ -66,7 +68,7 @@ class InvoiceController extends BaseController } - public function store(StoreInvoiceRequest $request) + public function store(StoreShopInvoiceRequest $request) { $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); @@ -80,7 +82,7 @@ class InvoiceController extends BaseController $client = Client::find($request->input('client_id')); - $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company_id, $company->owner()->id)); + $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company->id, $company->owner()->id)); event(new InvoiceWasCreated($invoice, $company, Ninja::eventVars())); diff --git a/app/Http/Requests/Shop/StoreShopClientRequest.php b/app/Http/Requests/Shop/StoreShopClientRequest.php new file mode 100644 index 0000000000..c7ee2497df --- /dev/null +++ b/app/Http/Requests/Shop/StoreShopClientRequest.php @@ -0,0 +1,183 @@ +input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.' . $index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + /* Ensure we have a client name, and that all emails are unique*/ + //$rules['name'] = 'required|min:1'; + $rules['id_number'] = 'unique:clients,id_number,' . $this->id . ',id,company_id,' . $this->company_id; + $rules['settings'] = new ValidClientGroupSettingsRule(); + $rules['contacts.*.email'] = 'nullable|distinct'; + $rules['contacts.*.password'] = [ + 'nullable', + 'sometimes', + 'string', + 'min:7', // must be at least 10 characters in length + 'regex:/[a-z]/', // must contain at least one lowercase letter + 'regex:/[A-Z]/', // must contain at least one uppercase letter + 'regex:/[0-9]/', // must contain at least one digit + //'regex:/[@$!%*#?&.]/', // must contain a special character + ]; + + if($this->company->account->isFreeHostedClient()) + $rules['hosted_clients'] = new CanStoreClientsRule($this->company->id); + + return $rules; + } + + + protected function prepareForValidation() + { + $this->company = Company::where('company_key', request()->header('X-API-COMPANY-KEY'))->firstOrFail(); + + $input = $this->all(); + + //@todo implement feature permissions for > 100 clients + // + $settings = ClientSettings::defaults(); + + if (array_key_exists('settings', $input) && !empty($input['settings'])) { + foreach ($input['settings'] as $key => $value) { + $settings->{$key} = $value; + } + } + + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + //is no settings->currency_id is set then lets dive in and find either a group or company currency all the below may be redundant!! + if (!property_exists($settings, 'currency_id') && isset($input['group_settings_id'])) { + $input['group_settings_id'] = $this->decodePrimaryKey($input['group_settings_id']); + $group_settings = GroupSetting::find($input['group_settings_id']); + + if ($group_settings && property_exists($group_settings->settings, 'currency_id') && isset($group_settings->settings->currency_id)) { + $settings->currency_id = (string)$group_settings->settings->currency_id; + } else { + $settings->currency_id = (string)$this->company->settings->currency_id; + } + } elseif (!property_exists($settings, 'currency_id')) { + $settings->currency_id = (string)$this->company->settings->currency_id; + } + + if (isset($input['currency_code'])) { + $settings->currency_id = $this->getCurrencyCode($input['currency_code']); + } + + $input['settings'] = $settings; + + if (isset($input['contacts'])) { + foreach ($input['contacts'] as $key => $contact) { + if (array_key_exists('id', $contact) && is_numeric($contact['id'])) { + unset($input['contacts'][$key]['id']); + } elseif (array_key_exists('id', $contact) && is_string($contact['id'])) { + $input['contacts'][$key]['id'] = $this->decodePrimaryKey($contact['id']); + } + + + //Filter the client contact password - if it is sent with ***** we should ignore it! + if (isset($contact['password'])) { + if (strlen($contact['password']) == 0) { + $input['contacts'][$key]['password'] = ''; + } else { + $contact['password'] = str_replace("*", "", $contact['password']); + + if (strlen($contact['password']) == 0) { + unset($input['contacts'][$key]['password']); + } + } + } + } + } + + if(isset($input['country_code'])) { + $input['country_id'] = $this->getCountryCode($input['country_code']); + } + + if(isset($input['shipping_country_code'])) { + $input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']); + } + + $this->replace($input); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + //'required' => trans('validation.required', ['attribute' => 'email']), + 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), + ]; + } + + private function getCountryCode($country_code) + { + $countries = Cache::get('countries'); + + $country = $countries->filter(function ($item) use($country_code) { + return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code; + })->first(); + + return (string) $country->id; + } + + private function getCurrencyCode($code) + { + $currencies = Cache::get('currencies'); + + $currency = $currencies->filter(function ($item) use($code){ + return $item->code == $code; + })->first(); + + return (string) $currency->id; + } + +} diff --git a/app/Http/Requests/Shop/StoreShopInvoiceRequest.php b/app/Http/Requests/Shop/StoreShopInvoiceRequest.php new file mode 100644 index 0000000000..4b895a5e63 --- /dev/null +++ b/app/Http/Requests/Shop/StoreShopInvoiceRequest.php @@ -0,0 +1,109 @@ +input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.' . $index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + $rules['client_id'] = 'required|exists:clients,id,company_id,'.$this->company->id; + + $rules['invitations.*.client_contact_id'] = 'distinct'; + + $rules['number'] = new UniqueInvoiceNumberRule($this->all()); + + return $rules; + } + + protected function prepareForValidation() + { + $this->company = Company::where('company_key', request()->header('X-API-COMPANY-KEY'))->firstOrFail(); + + $input = $this->all(); + + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + if (isset($input['client_contacts'])) { + foreach ($input['client_contacts'] as $key => $contact) { + if (!array_key_exists('send_email', $contact) || !array_key_exists('id', $contact)) { + unset($input['client_contacts'][$key]); + } + } + } + + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } + + if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + //$input['line_items'] = json_encode($input['line_items']); + $this->replace($input); + } +} From 2739e643fe62ab26c157788a8d55fa0ed651886d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 28 Jul 2020 23:47:41 +1000 Subject: [PATCH 18/26] Fixes for invoicewasvieweD --- app/Http/Controllers/Shop/InvoiceController.php | 2 +- app/Listeners/Invoice/InvoiceViewedActivity.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Shop/InvoiceController.php b/app/Http/Controllers/Shop/InvoiceController.php index 3a70c75a73..aabd624549 100644 --- a/app/Http/Controllers/Shop/InvoiceController.php +++ b/app/Http/Controllers/Shop/InvoiceController.php @@ -52,7 +52,7 @@ class InvoiceController extends BaseController $this->invoice_repo = $invoice_repo; } - public function show(string $invitation_key) + public function show(Request $request, string $invitation_key) { $company = Company::where('company_key', $request->header('X-API-COMPANY-KEY'))->first(); diff --git a/app/Listeners/Invoice/InvoiceViewedActivity.php b/app/Listeners/Invoice/InvoiceViewedActivity.php index b3afaf4032..72239bf06f 100644 --- a/app/Listeners/Invoice/InvoiceViewedActivity.php +++ b/app/Listeners/Invoice/InvoiceViewedActivity.php @@ -54,6 +54,6 @@ class InvoiceViewedActivity implements ShouldQueue $fields->invitation_id = $event->invitation->id; $fields->invoice_id = $event->invitation->invoice_id; - $this->activity_repo->save($fields, $event->invoice, $event->event_vars); + $this->activity_repo->save($fields, $event->invitation->invoice, $event->event_vars); } } From 0c7a2cd77696a8213efd8f5ae066205190cda9f9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 00:11:43 +1000 Subject: [PATCH 19/26] formatting in auth.php" --- config/auth.php | 1 - 1 file changed, 1 deletion(-) diff --git a/config/auth.php b/config/auth.php index 75a46ab805..dc92eeb0c3 100644 --- a/config/auth.php +++ b/config/auth.php @@ -78,7 +78,6 @@ return [ ], 'contacts' => [ 'driver' => 'eloquent', - 'model' => App\Models\ClientContact::class, ], From 089ededb7f57b55d1f82ea1c4cbb53602250435f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 08:07:58 +1000 Subject: [PATCH 20/26] Tests for shop route --- tests/Feature/Shop/ShopInvoiceTest.php | 44 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/Feature/Shop/ShopInvoiceTest.php b/tests/Feature/Shop/ShopInvoiceTest.php index e921185ff1..845d59d8b1 100644 --- a/tests/Feature/Shop/ShopInvoiceTest.php +++ b/tests/Feature/Shop/ShopInvoiceTest.php @@ -7,6 +7,7 @@ use App\Models\CompanyToken; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Validation\ValidationException; use Tests\MockAccountData; use Tests\TestCase; @@ -37,21 +38,34 @@ class ShopInvoiceTest extends TestCase $this->withoutExceptionHandling(); } - public function testTokenFailure() + public function testTokenSuccess() { + $this->company->enable_shop_api = true; + $this->company->save(); + $response = null; + + try { + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-COMPANY-KEY' => $this->company->company_key - ])->get('/api/v1/shop/products'); + ])->get('api/v1/shop/products'); + } + catch (ValidationException $e) { + $this->assertNotNull($message); + } $response->assertStatus(200); } - public function testTokenSuccess() + public function testTokenFailure() { + $this->company->enable_shop_api = true; + $this->company->save(); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-COMPANY-KEY' => $this->company->company_key @@ -63,8 +77,29 @@ class ShopInvoiceTest extends TestCase $arr = $response->json(); } + public function testCompanyEnableShopApiBooleanWorks() + { + try { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->get('api/v1/shop/products'); + } + + catch (ValidationException $e) { + $this->assertNotNull($message); + } + + $response->assertStatus(403); + } + public function testGetByProductKey() { + + $this->company->enable_shop_api = true; + $this->company->save(); + $product = factory(\App\Models\Product::class)->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, @@ -86,6 +121,9 @@ class ShopInvoiceTest extends TestCase public function testGetByClientByContactKey() { + $this->company->enable_shop_api = true; + $this->company->save(); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-COMPANY-KEY' => $this->company->company_key From 0f5eb27a039a0bc7c32d1791240d4cd87f257b13 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 08:11:51 +1000 Subject: [PATCH 21/26] Tests for POST routes on clients and invoices shop routes --- tests/Feature/Shop/ShopInvoiceTest.php | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/Feature/Shop/ShopInvoiceTest.php b/tests/Feature/Shop/ShopInvoiceTest.php index 845d59d8b1..e661c9e358 100644 --- a/tests/Feature/Shop/ShopInvoiceTest.php +++ b/tests/Feature/Shop/ShopInvoiceTest.php @@ -136,4 +136,69 @@ class ShopInvoiceTest extends TestCase $this->assertEquals($this->client->hashed_id, $arr['data']['id']); } + + public function testCreateClientOnShopRoute() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + + $data = [ + 'name' => 'ShopClient', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->post('/api/v1/shop/clients/', $data); + + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('ShopClient', $arr['data']['name']); + + } + + public function testCreateInvoiceOnShopRoute() + { + + $this->company->enable_shop_api = true; + $this->company->save(); + + $data = [ + 'name' => 'ShopClient', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->post('/api/v1/shop/clients/', $data); + + + $response->assertStatus(200); + $arr = $response->json(); + + $client_hashed_id = $arr['data']['id']; + + $invoice_data = [ + 'client_id' => $client_hashed_id, + 'po_number' => 'shop_order' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-COMPANY-KEY' => $this->company->company_key + ])->post('/api/v1/shop/invoices/', $invoice_data); + + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('shop_order', $arr['data']['po_number']); + + + } + } From b457cbfe15eefbdde49c4b4c520692e314b43e78 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 08:20:40 +1000 Subject: [PATCH 22/26] Fix is_manual when marking an invoice as paid --- app/Services/Invoice/MarkPaid.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Services/Invoice/MarkPaid.php b/app/Services/Invoice/MarkPaid.php index 76591becad..be9c38d562 100644 --- a/app/Services/Invoice/MarkPaid.php +++ b/app/Services/Invoice/MarkPaid.php @@ -56,6 +56,7 @@ class MarkPaid extends AbstractService $payment->client_id = $this->invoice->client_id; $payment->transaction_reference = ctrans('texts.manual_entry'); $payment->currency_id = $this->invoice->client->getSetting('currency_id'); + $payment->is_manual = true; /* Create a payment relationship to the invoice entity */ $payment->save(); From ce3ce8a54bac2a8d76ace001f3a83cb892f917df Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 08:23:35 +1000 Subject: [PATCH 23/26] Change invoice_number filtering to number filtering --- app/Filters/InvoiceFilters.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 35766435dd..9b6aabd97a 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -70,9 +70,9 @@ class InvoiceFilters extends QueryFilters return $this->builder; } - public function invoice_number(string $invoice_number):Builder + public function number(string $number) :Builder { - return $this->builder->where('number', $invoice_number); + return $this->builder->where('number', $number); } /** From 65105362349bc9cce1f5792c5a05b5fa3f12dd31 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 12:13:12 +1000 Subject: [PATCH 24/26] Fixes for view_link in emails --- .../Requests/Invoice/UpdateInvoiceRequest.php | 7 ++++++ app/Mail/BouncedEmail.php | 2 +- app/Mail/VerifyUser.php | 2 +- app/Notifications/BaseNotification.php | 20 +++++++++++++++- app/Notifications/SendGenericNotification.php | 3 ++- app/Repositories/ClientContactRepository.php | 3 +++ app/Utils/HtmlEngine.php | 23 ++++++------------- app/Utils/Traits/MakesInvoiceValues.php | 6 ++++- 8 files changed, 45 insertions(+), 21 deletions(-) diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 7dba25743d..01f2cbedc5 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -94,4 +94,11 @@ class UpdateInvoiceRequest extends Request $this->replace($input); } + + public function messages() + { + return [ + 'id' => ctrans('text.locked_invoice'), + ]; + } } diff --git a/app/Mail/BouncedEmail.php b/app/Mail/BouncedEmail.php index 7886967cb6..68876078cc 100644 --- a/app/Mail/BouncedEmail.php +++ b/app/Mail/BouncedEmail.php @@ -59,7 +59,7 @@ class BouncedEmail extends Mailable implements ShouldQueue //->bcc('') ->queue(new BouncedEmail($invitation)); - return $this->from('turbo124@gmail.com') //todo + return $this->from('x@gmail.com') //todo ->subject(ctrans('texts.confirmation_subject')) ->markdown('email.auth.verify', ['user' => $this->user]) ->text('email.auth.verify_text'); diff --git a/app/Mail/VerifyUser.php b/app/Mail/VerifyUser.php index 13d4587736..9ef44712b4 100644 --- a/app/Mail/VerifyUser.php +++ b/app/Mail/VerifyUser.php @@ -39,7 +39,7 @@ class VerifyUser extends Mailable implements ShouldQueue */ public function build() { - return $this->from('turbo124@gmail.com') //todo + return $this->from('x@gmail.com') //todo ->subject(ctrans('texts.confirmation_subject')) ->markdown('email.auth.verify', ['user' => $this->user]) ->text('email.auth.verify_text'); diff --git a/app/Notifications/BaseNotification.php b/app/Notifications/BaseNotification.php index 5b9d71b768..293cc145c8 100644 --- a/app/Notifications/BaseNotification.php +++ b/app/Notifications/BaseNotification.php @@ -103,7 +103,7 @@ class BaseNotification extends Notification implements ShouldQueue $email_style_custom = $this->settings->email_style_custom; $body = strtr($email_style_custom, "$body", $body); } - + $data = [ 'body' => $body, 'design' => $design_style, @@ -120,4 +120,22 @@ class BaseNotification extends Notification implements ShouldQueue return $data; } + + public function getTemplateView() + { + + switch ($this->settings->email_style) { + case 'plain': + return 'email.template.plain'; + break; + case 'custom': + return 'email.template.custom'; + break; + default: + return 'email.admin.generic_email'; + break; + } + + } + } \ No newline at end of file diff --git a/app/Notifications/SendGenericNotification.php b/app/Notifications/SendGenericNotification.php index fa1395900d..0ccbe6996e 100644 --- a/app/Notifications/SendGenericNotification.php +++ b/app/Notifications/SendGenericNotification.php @@ -73,10 +73,11 @@ class SendGenericNotification extends BaseNotification implements ShouldQueue */ public function toMail($notifiable) { + $mail_message = (new MailMessage) ->withSwiftMessage(function ($message) { $message->getHeaders()->addTextHeader('Tag', $this->invitation->company->company_key); - })->markdown('email.admin.generic_email', $this->buildMailMessageData()); + })->markdown($this->getTemplateView(), $this->buildMailMessageData()); $mail_message = $this->buildMailMessageSettings($mail_message); diff --git a/app/Repositories/ClientContactRepository.php b/app/Repositories/ClientContactRepository.php index 0ef048db94..ac67a055e8 100644 --- a/app/Repositories/ClientContactRepository.php +++ b/app/Repositories/ClientContactRepository.php @@ -70,6 +70,9 @@ class ClientContactRepository extends BaseRepository }); + //need to reload here to shake off stale contacts + $client->load('contacts'); + //always made sure we have one blank contact to maintain state if ($client->contacts->count() == 0) { diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index fc922bc5f3..8acbce3f89 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -84,22 +84,10 @@ class HtmlEngine - - - - - - - - - - - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private function buildEntityDataArray() :array + public function buildEntityDataArray() :array { if (!$this->client->currency()) { throw new \Exception(debug_backtrace()[1]['function'], 1); @@ -132,21 +120,24 @@ class HtmlEngine $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.invoice_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; + } if ($this->entity_string == 'quote') { $data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.quote')]; $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.quote_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.quote_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_quote').'', 'label' => ctrans('texts.view_quote')]; + } if ($this->entity_string == 'credit') { $data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')]; $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.credit_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.credit_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_credit').'', 'label' => ctrans('texts.view_credit')]; + } $data['$entity_number'] = &$data['$number']; diff --git a/app/Utils/Traits/MakesInvoiceValues.php b/app/Utils/Traits/MakesInvoiceValues.php index 626241f578..e5d573c544 100644 --- a/app/Utils/Traits/MakesInvoiceValues.php +++ b/app/Utils/Traits/MakesInvoiceValues.php @@ -187,6 +187,7 @@ trait MakesInvoiceValues } $calc = $this->calc(); + $invitation = $this->invitations->where('client_contact_id', $contact->id)->first(); $data = []; $data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; @@ -214,6 +215,7 @@ trait MakesInvoiceValues $data['$number'] = ['value' => $this->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$entity.terms'] = ['value' => $this->terms ?: ' ', 'label' => ctrans('texts.invoice_terms')]; $data['$terms'] = &$data['$entity.terms']; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; } if ($this instanceof Quote) { @@ -221,13 +223,15 @@ trait MakesInvoiceValues $data['$number'] = ['value' => $this->number ?: ' ', 'label' => ctrans('texts.quote_number')]; $data['$entity.terms'] = ['value' => $this->terms ?: ' ', 'label' => ctrans('texts.quote_terms')]; $data['$terms'] = &$data['$entity.terms']; - } + $data['$view_link'] = ['value' => ''. ctrans('texts.view_quote').'', 'label' => ctrans('texts.view_quote')]; + } if ($this instanceof Credit) { $data['$entity_label'] = ['value' => '', 'label' => ctrans('texts.credit')]; $data['$number'] = ['value' => $this->number ?: ' ', 'label' => ctrans('texts.credit_number')]; $data['$entity.terms'] = ['value' => $this->terms ?: ' ', 'label' => ctrans('texts.credit_terms')]; $data['$terms'] = &$data['$entity.terms']; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_credit').'', 'label' => ctrans('texts.view_credit')]; } $data['$entity_number'] = &$data['$number']; From 8e0942688de33fc6392fefdb066eb68dcbd89684 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 15:14:55 +1000 Subject: [PATCH 25/26] fixes for tests --- app/DataMapper/EmailTemplateDefaults.php | 1 - app/Notifications/SendGenericNotification.php | 4 +++- app/Utils/HtmlEngine.php | 6 +++--- tests/MockAccountData.php | 2 ++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/DataMapper/EmailTemplateDefaults.php b/app/DataMapper/EmailTemplateDefaults.php index eb831a516f..e92fda3da9 100644 --- a/app/DataMapper/EmailTemplateDefaults.php +++ b/app/DataMapper/EmailTemplateDefaults.php @@ -121,7 +121,6 @@ class EmailTemplateDefaults return $converter->convertToHtml(self::transformText('invoice_message')); - //return Parsedown::instance()->line(self::transformText('invoice_message')); } public static function emailQuoteSubject() diff --git a/app/Notifications/SendGenericNotification.php b/app/Notifications/SendGenericNotification.php index 0ccbe6996e..097047c023 100644 --- a/app/Notifications/SendGenericNotification.php +++ b/app/Notifications/SendGenericNotification.php @@ -77,11 +77,13 @@ class SendGenericNotification extends BaseNotification implements ShouldQueue $mail_message = (new MailMessage) ->withSwiftMessage(function ($message) { $message->getHeaders()->addTextHeader('Tag', $this->invitation->company->company_key); - })->markdown($this->getTemplateView(), $this->buildMailMessageData()); + //})->markdown($this->getTemplateView(), $this->buildMailMessageData()); + })->markdown('email.template.plain', $this->buildMailMessageData()); $mail_message = $this->buildMailMessageSettings($mail_message); return $mail_message; + } /** diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 8acbce3f89..3a5152d279 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -120,7 +120,7 @@ class HtmlEngine $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.invoice_terms')]; $data['$terms'] = &$data['$entity.terms']; - $data['$view_link'] = ['value' => ''. ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_invoice').'', 'label' => ctrans('texts.view_invoice')]; } if ($this->entity_string == 'quote') { @@ -128,7 +128,7 @@ class HtmlEngine $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.quote_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.quote_terms')]; $data['$terms'] = &$data['$entity.terms']; - $data['$view_link'] = ['value' => ''. ctrans('texts.view_quote').'', 'label' => ctrans('texts.view_quote')]; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_quote').'', 'label' => ctrans('texts.view_quote')]; } if ($this->entity_string == 'credit') { @@ -136,7 +136,7 @@ class HtmlEngine $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.credit_number')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: ' ', 'label' => ctrans('texts.credit_terms')]; $data['$terms'] = &$data['$entity.terms']; - $data['$view_link'] = ['value' => ''. ctrans('texts.view_credit').'', 'label' => ctrans('texts.view_credit')]; + $data['$view_link'] = ['value' => ''. ctrans('texts.view_credit').'', 'label' => ctrans('texts.view_credit')]; } $data['$entity_number'] = &$data['$number']; diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 18c02ecc30..35096ddc2f 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -225,6 +225,7 @@ trait MockAccountData $this->quote = $this->quote_calc->getQuote(); $this->quote->number = $this->getNextQuoteNumber($this->client); + $this->quote->service()->createInvitations()->markSent(); $this->quote->setRelation('client', $this->client); $this->quote->setRelation('company', $this->company); @@ -242,6 +243,7 @@ trait MockAccountData $this->credit->save(); + $this->credit->service()->createInvitations()->markSent(); $this->credit_calc = new InvoiceSum($this->credit); $this->credit_calc->build(); From 411b4b15829885d75c555c533a4c67ec3b4702e9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 29 Jul 2020 19:25:59 +1000 Subject: [PATCH 26/26] add enable_shop_api field to company table --- app/Models/Company.php | 1 + app/Transformers/CompanyTransformer.php | 1 + tests/Integration/DesignTest.php | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/Company.php b/app/Models/Company.php index 2dc887c1f1..a8c3fa440f 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -109,6 +109,7 @@ class Company extends BaseModel 'slack_webhook_url', 'google_analytics_key', 'client_can_register', + 'enable_shop_api', ]; diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 6a31e1d616..a4709aa506 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -132,6 +132,7 @@ class CompanyTransformer extends EntityTransformer 'enabled_item_tax_rates' => (int) $company->enabled_item_tax_rates, 'client_can_register' => (bool) $company->client_can_register, 'is_large' => (bool) $company->is_large, + 'enable_shop_api' => (bool) $company->enable_shop_api, ]; } diff --git a/tests/Integration/DesignTest.php b/tests/Integration/DesignTest.php index b311d5a1a6..1a4fdd71d6 100644 --- a/tests/Integration/DesignTest.php +++ b/tests/Integration/DesignTest.php @@ -92,7 +92,6 @@ class DesignTest extends TestCase $this->assertNotNull($html); - $this->quote = factory(\App\Models\Invoice::class)->create([ 'user_id' => $this->user->id, 'client_id' => $this->client->id,