Merge pull request #3939 from turbo124/v2

Shop routes!
This commit is contained in:
David Bomba 2020-07-28 22:07:14 +10:00 committed by GitHub
commit 444df3d850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 459 additions and 13 deletions

View File

@ -286,6 +286,7 @@ class CreateTestData extends Command
$company = factory(\App\Models\Company::class)->create([ $company = factory(\App\Models\Company::class)->create([
'account_id' => $account->id, 'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'), 'slack_webhook_url' => config('ninja.notification.slack'),
'is_large' => true,
]); ]);
$account->default_company_id = $company->id; $account->default_company_id = $company->id;

View File

@ -33,8 +33,8 @@ class ClientFactory
$client->client_hash = Str::random(40); $client->client_hash = Str::random(40);
$client->settings = ClientSettings::defaults(); $client->settings = ClientSettings::defaults();
$client_contact = ClientContactFactory::create($company_id, $user_id); // $client_contact = ClientContactFactory::create($company_id, $user_id);
$client->contacts->add($client_contact); // $client->contacts->add($client_contact);
return $client; return $client;
} }

View File

@ -270,7 +270,7 @@ class BaseController extends Controller
$query->with($includes); $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); $query->where('user_id', '=', auth()->user()->id);
} }
@ -346,7 +346,7 @@ class BaseController extends Controller
$data = $this->createItem($item, $transformer, $this->entity_type); $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()); $data['static'] = Statics::company(auth()->user()->getCompany()->getLocale());
} }

View File

@ -54,7 +54,7 @@ class InvitationController extends Controller
event(new InvitationWasViewed($invitation->{$entity}, $invitation, $invitation->{$entity}->company, Ninja::eventVars())); 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})]); return redirect()->route('client.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->{$key})]);

View File

@ -0,0 +1,82 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers\Shop;
use App\Events\Client\ClientWasCreated;
use App\Factory\ClientFactory;
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;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class ClientController extends BaseController
{
use MakesHash;
protected $entity_type = Client::class;
protected $entity_transformer = ClientTransformer::class;
/**
* @var ClientRepository
*/
protected $client_repo;
/**
* ClientController constructor.
* @param ClientRepository $clientRepo
*/
public function __construct(ClientRepository $client_repo)
{
parent::__construct();
$this->client_repo = $client_repo;
}
public function show(Request $request, string $contact_key)
{
$company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first();
$contact = ClientContact::with('client')
->where('company_id', $company->id)
->where('contact_key', $contact_key)
->firstOrFail();
return $this->itemResponse($contact->client);
}
public function store(StoreClientRequest $request)
{
$company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first();
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'), $company, $client);
event(new ClientWasCreated($client, $company, Ninja::eventVars()));
return $this->itemResponse($client);
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
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;
use App\Models\Client;
use App\Models\CompanyToken;
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;
class InvoiceController extends BaseController
{
use MakesHash;
protected $entity_type = Invoice::class;
protected $entity_transformer = InvoiceTransformer::class;
/**
* @var InvoiceRepository
*/
protected $invoice_repo;
/**
* InvoiceController constructor.
*
* @param \App\Repositories\InvoiceRepository $invoice_repo The invoice repo
*/
public function __construct(InvoiceRepository $invoice_repo)
{
parent::__construct();
$this->invoice_repo = $invoice_repo;
}
public function show(string $invitation_key)
{
$company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first();
$invitation = InvoiceInvitation::with(['invoice'])
->where('company_id', $company->id)
->where('key',$invitation_key)
->firstOrFail();
return $this->itemResponse($invitation->invoice);
}
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();
$client = Client::find($request->input('client_id'));
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create($company_id, $company->owner()->id));
event(new InvoiceWasCreated($invoice, $company, Ninja::eventVars()));
$invoice = $invoice->service()->triggeredActions($request)->save();
return $this->itemResponse($invoice);
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
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;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class ProductController extends BaseController
{
use MakesHash;
protected $entity_type = Product::class;
protected $entity_transformer = ProductTransformer::class;
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first();
$products = Product::where('company_id', $company->id);
return $this->listResponse($products);
}
public function show(Request $request, string $product_key)
{
$company = Company::where('company_key', $request->header('X-API-COMPANY_KEY'))->first();
$product = Product::where('company_id', $company->id)
->where('product_key', $product_key)
->first();
return $this->itemResponse($product);
}
}

View File

@ -73,6 +73,11 @@ class Kernel extends HttpKernel
\App\Http\Middleware\StartupCheck::class, \App\Http\Middleware\StartupCheck::class,
\App\Http\Middleware\QueryLogging::class, \App\Http\Middleware\QueryLogging::class,
], ],
'shop' => [
'throttle:60,1',
'bindings',
'query_logging',
],
]; ];
/** /**
@ -106,7 +111,10 @@ class Kernel extends HttpKernel
'url_db' => \App\Http\Middleware\UrlSetDb::class, 'url_db' => \App\Http\Middleware\UrlSetDb::class,
'web_db' => \App\Http\Middleware\SetWebDb::class, 'web_db' => \App\Http\Middleware\SetWebDb::class,
'api_db' => \App\Http\Middleware\SetDb::class, 'api_db' => \App\Http\Middleware\SetDb::class,
'company_key_db' => \App\Http\Middleware\SetDbByCompanyKey::class,
'locale' => \App\Http\Middleware\Locale::class, 'locale' => \App\Http\Middleware\Locale::class,
'contact.register' => \App\Http\Middleware\ContactRegister::class, 'contact.register' => \App\Http\Middleware\ContactRegister::class,
'shop_token_auth' => \App\Http\Middleware\Shop\ShopTokenAuth::class,
]; ];
} }

View File

@ -0,0 +1,48 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Middleware;
use App\Libraries\MultiDB;
use App\Models\CompanyToken;
use Closure;
class SetDbByCompanyKey
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$error = [
'message' => '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);
}
}

View File

@ -29,6 +29,7 @@ class TokenAuth
public function handle($request, Closure $next) 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 ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user','company'])->whereRaw("BINARY `token`= ?", [$request->header('X-API-TOKEN')])->first())) {
$user = $company_token->user; $user = $company_token->user;
$error = [ $error = [

View File

@ -180,6 +180,17 @@ class MultiDB
return false; 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 public static function findAndSetDbByDomain($subdomain) :bool
{ {
foreach (self::$dbs as $db) { foreach (self::$dbs as $db) {

View File

@ -46,10 +46,10 @@ class InvoiceViewedActivity implements ShouldQueue
$fields = new \stdClass; $fields = new \stdClass;
$fields->user_id = $event->invoice->user_id; $fields->user_id = $event->invitation->user_id;
$fields->company_id = $event->invoice->company_id; $fields->company_id = $event->invitation->company_id;
$fields->activity_type_id = Activity::VIEW_INVOICE; $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->client_contact_id = $event->invitation->client_contact_id;
$fields->invitation_id = $event->invitation->id; $fields->invitation_id = $event->invitation->id;
$fields->invoice_id = $event->invitation->invoice_id; $fields->invoice_id = $event->invitation->invoice_id;

View File

@ -46,6 +46,7 @@ class CompanyUser extends Pivot
'is_owner', 'is_owner',
'is_locked', 'is_locked',
'slack_webhook_url', 'slack_webhook_url',
'shop_restricted'
]; ];
protected $touches = []; protected $touches = [];

View File

@ -57,6 +57,8 @@ class RouteServiceProvider extends ServiceProvider
$this->mapContactApiRoutes(); $this->mapContactApiRoutes();
$this->mapClientApiRoutes(); $this->mapClientApiRoutes();
$this->mapShopApiRoutes();
} }
/** /**
@ -117,4 +119,12 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace) ->namespace($this->namespace)
->group(base_path('routes/client.php')); ->group(base_path('routes/client.php'));
} }
protected function mapShopApiRoutes()
{
Route::prefix('')
->middleware('shop')
->namespace($this->namespace)
->group(base_path('routes/shop.php'));
}
} }

View File

@ -78,7 +78,7 @@ class ClientRepository extends BaseRepository
$data['name'] = $client->present()->name(); $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)) { if (array_key_exists('documents', $data)) {
$this->saveDocuments($data['documents'], $client); $this->saveDocuments($data['documents'], $client);

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ShopToken extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Blueprint $table) {
$table->boolean('enable_shop_api')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -136,8 +136,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('emails', 'EmailController@send')->name('email.send'); Route::post('emails', 'EmailController@send')->name('email.send');
/*Subscription and Webhook routes */ /*Subscription and Webhook routes */
Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); // Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe');
Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); // Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe');
Route::resource('webhooks', 'WebhookController'); Route::resource('webhooks', 'WebhookController');
Route::post('webhooks/bulk', 'WebhookController@bulk')->name('webhooks.bulk'); Route::post('webhooks/bulk', 'WebhookController@bulk')->name('webhooks.bulk');

14
routes/shop.php Normal file
View File

@ -0,0 +1,14 @@
<?php
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::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');
});

View File

@ -0,0 +1,101 @@
<?php
namespace Tests\Feature\Shop;
use App\Factory\CompanyUserFactory;
use App\Models\CompanyToken;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\Shop\InvoiceController
*/
class ShopInvoiceTest extends TestCase
{
use MakesHash;
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->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']);
}
}

View File

@ -126,8 +126,8 @@ class FactoryCreationTest extends TestCase
$cliz->save(); $cliz->save();
$this->assertNotNull($cliz->contacts); $this->assertNotNull($cliz->contacts);
$this->assertEquals(1, $cliz->contacts->count()); $this->assertEquals(0, $cliz->contacts->count());
$this->assertInternalType("int", $cliz->contacts->first()->id); $this->assertInternalType("int", $cliz->id);
} }
/** /**