diff --git a/.env.example b/.env.example index 3b1492054268..f59737178660 100644 --- a/.env.example +++ b/.env.example @@ -47,3 +47,4 @@ MULTI_DB_ENABLED=true POSTMARK_API_TOKEN= GOOGLE_MAPS_API_KEY= +API_SECRET=superdoopersecrethere diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 5dffcea7af56..cd2ee614c591 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers; use App\DataMapper\ClientSettings; -use App\Datatables\MakesActionMenu; use App\Factory\ClientFactory; use App\Http\Requests\Client\CreateClientRequest; use App\Http\Requests\Client\EditClientRequest; @@ -20,7 +19,6 @@ use App\Models\Currency; use App\Models\Size; use App\Repositories\ClientRepository; use App\Utils\Traits\MakesHash; -use App\Utils\Traits\MakesMenu; use App\Utils\Traits\UserSessionAttributes; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -34,8 +32,6 @@ class ClientController extends Controller { use UserSessionAttributes; use MakesHash; - use MakesMenu; - use MakesActionMenu; /** * @var ClientRepository @@ -70,17 +66,16 @@ class ClientController extends Controller public function show(ShowClientRequest $request, Client $client) { - $data = [ 'client' => $client, - 'company' => auth()->user()->company(), + 'company' => $client->company(), 'meta' => collect([ 'google_maps_api_key' => config('ninja.google_maps_api_key') ]) ]; - return response()->json($data); - + //return response()->json($data); + return redirect()->route('clients.edit', ['id' => $this->encodePrimarykey($client->id)]); } /** @@ -94,9 +89,9 @@ class ClientController extends Controller $data = [ 'client' => $client, - 'settings' => collect(ClientSettings::buildClientSettings(auth()->user()->company()->settings_object, $client->client_settings_object)), + 'settings' => collect(ClientSettings::buildClientSettings($client->company()->settings_object, $client->client_settings_object)), 'hashed_id' => $this->encodePrimarykey($client->id), - 'company' => auth()->user()->company(), + 'company' => $client->company(), 'sizes' => Size::all(), ]; diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index de3f6c7d3da8..28f17de88ecc 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -70,6 +70,8 @@ class Kernel extends HttpKernel 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'query_logging' => \App\Http\Middleware\QueryLogging::class, + 'token_auth' => \App\Http\Middleware\TokenAuth::class, + 'api_secret_check' => \App\Http\Middleware\ApiSecretCheck::class, ]; } diff --git a/app/Http/Middleware/ApiSecretCheck.php b/app/Http/Middleware/ApiSecretCheck.php new file mode 100644 index 000000000000..415004099258 --- /dev/null +++ b/app/Http/Middleware/ApiSecretCheck.php @@ -0,0 +1,34 @@ +header('X-API-SECRET') && ($request->header('X-API-SECRET') == config('ninja.api_secret')) ) + { + return $next($request); + } + else { + + $error['error'] = ['message' => 'Invalid secret']; + + return response()->json(json_encode($error, JSON_PRETTY_PRINT) ,403); + } + + + } +} diff --git a/app/Http/Middleware/TokenAuth.php b/app/Http/Middleware/TokenAuth.php index 8cf052362660..30e3a3a8fd7b 100644 --- a/app/Http/Middleware/TokenAuth.php +++ b/app/Http/Middleware/TokenAuth.php @@ -18,8 +18,8 @@ class TokenAuth public function handle($request, Closure $next) { - if($request->header('X-API-TOKEN') - && ($user = CompanyToken::whereRaw("BINARY `token`= ?",[$request->header('X-API-TOKEN')])->user)) { + if( $request->header('X-API-TOKEN') && ($user = CompanyToken::whereRaw("BINARY `token`= ?",[$request->header('X-API-TOKEN')])->first()->user ) ) + { auth()->login($user); diff --git a/app/Models/Account.php b/app/Models/Account.php index 9ad9ca859cfc..619c96177675 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -50,7 +50,7 @@ class Account extends BaseModel public function default_company() { - return $this->hasOne(Company::class, 'default_company_id', 'id'); + return $this->hasOne(Company::class, 'id', 'default_company_id'); } /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo diff --git a/app/Models/User.php b/app/Models/User.php index 0506253fc7f6..4b96503ddb81 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -60,6 +60,16 @@ class User extends Authenticatable implements MustVerifyEmail 'slack_webhook_url', ]; + public function token() + { + return $this->tokens->first(); + } + + public function tokens() + { + return $this->hasMany(CompanyToken::class)->orderBy('id'); + } + /** * Returns all companies a user has access to. * diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 9a85e837e50e..acb0285d5fe1 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -119,9 +119,12 @@ class RouteServiceProvider extends ServiceProvider */ protected function mapApiRoutes() { - Route::prefix('api') + Route::prefix('api/v1') ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); } + + + } diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php new file mode 100644 index 000000000000..82159b3e63a6 --- /dev/null +++ b/app/Transformers/AccountTransformer.php @@ -0,0 +1,54 @@ + $this->encodePrimaryKey($account->id), + ]; + } + + public function includeDefaultCompany(Account $account) + { + $transformer = new CompanyTransformer($this->serializer); + + return $this->includeItem($account->default_company, $transformer, Company::class); + } +} diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php new file mode 100644 index 000000000000..4431ec2d6f38 --- /dev/null +++ b/app/Transformers/CompanyTransformer.php @@ -0,0 +1,77 @@ + $this->encodePrimaryKey($company->id), + 'name' => $company->name, + 'company_key' => $company->company_key, + 'last_login' => $company->last_login, + 'address1' => $company->address1, + 'address2' => $company->address2, + 'city' => $company->city, + 'state' => $company->state, + 'postal_code' => $company->postal_code, + 'work_phone' => $company->work_phone, + 'work_email' => $company->work_email, + 'country_id' => (int) $company->country_id, + 'subdomain' => $company->subdomain, + 'db' => $company->db, + 'vat_number' => $company->vat_number, + 'id_number' => $company->id_number, + 'size_id' => (int) $company->size_id, + 'industry_id' => (int) $company->industry_id, + 'settings' => $company->settings, + 'updated_at' => $user->updated_at, + 'deleted_at' => $user->deleted_at, + ]; + } + + +} diff --git a/app/Transformers/PaymentTransformer.php b/app/Transformers/PaymentTransformer.php new file mode 100644 index 000000000000..49656f0b05e6 --- /dev/null +++ b/app/Transformers/PaymentTransformer.php @@ -0,0 +1,77 @@ +invoice = $invoice; + } + + public function includeInvoice(Payment $payment) + { + $transformer = new InvoiceTransformer($this->account, $this->serializer); + + return $this->includeItem($payment->invoice, $transformer, 'invoice'); + } + + public function includeClient(Payment $payment) + { + $transformer = new ClientTransformer($this->account, $this->serializer); + + return $this->includeItem($payment->client, $transformer, 'client'); + } + + public function transform(Payment $payment) + { + return array_merge($this->getDefaults($payment), [ + 'id' => (int) $payment->public_id, + 'amount' => (float) $payment->amount, + 'transaction_reference' => $payment->transaction_reference ?: '', + 'payment_date' => $payment->payment_date ?: '', + 'updated_at' => $this->getTimestamp($payment->updated_at), + 'archived_at' => $this->getTimestamp($payment->deleted_at), + 'is_deleted' => (bool) $payment->is_deleted, + 'payment_type_id' => (int) ($payment->payment_type_id ?: 0), + 'invoice_id' => (int) ($this->invoice ? $this->invoice->public_id : $payment->invoice->public_id), + 'invoice_number' => $this->invoice ? $this->invoice->invoice_number : $payment->invoice->invoice_number, + 'private_notes' => $payment->private_notes ?: '', + 'exchange_rate' => (float) $payment->exchange_rate, + 'exchange_currency_id' => (int) $payment->exchange_currency_id, + 'refunded' => (float) $payment->refunded, + 'payment_status_id' => (int) ($payment->payment_status_id ?: PAYMENT_STATUS_COMPLETED), + ]); + } +} diff --git a/config/ninja.php b/config/ninja.php index 04fc189496b4..fff5c8a6b640 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -9,6 +9,7 @@ return [ 'app_version' => '0.1', 'terms_version' => '1.0.1', 'app_env' => env('APP_ENV', 'development'), + 'api_secret' => env('API_SECRET', ''), 'google_maps_api_key' => env('GOOGLE_MAPS_API_KEY'), 'environment' => env('NINJA_ENVIRONMENT', 'selfhost'), // 'hosted', 'development', 'selfhost', 'reseller' diff --git a/database/seeds/RandomDataSeeder.php b/database/seeds/RandomDataSeeder.php index b666736f8d37..d6617845bbdb 100644 --- a/database/seeds/RandomDataSeeder.php +++ b/database/seeds/RandomDataSeeder.php @@ -3,6 +3,7 @@ use App\Models\Account; use App\Models\Client; use App\Models\ClientContact; +use App\Models\CompanyToken; use App\Models\User; use App\Models\UserAccount; use Illuminate\Database\Seeder; @@ -38,6 +39,14 @@ class RandomDataSeeder extends Seeder 'confirmation_code' => $this->createDbHash(config('database.default')) ]); + $company_token = CompanyToken::create([ + 'user_id' => $user->id, + 'company_id' => $company->id, + 'account_id' => $account->id, + 'name' => 'test token', + 'token' => str_random(64), + ]); + $user->companies()->attach($company->id, [ 'account_id' => $account->id, 'is_owner' => 1, @@ -62,7 +71,7 @@ class RandomDataSeeder extends Seeder ]); - factory(\App\Models\Client::class, 50)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){ + factory(\App\Models\Client::class, 5)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){ factory(\App\Models\ClientContact::class,1)->create([ 'user_id' => $user->id, @@ -76,16 +85,7 @@ class RandomDataSeeder extends Seeder 'client_id' => $c->id, 'company_id' => $company->id ]); -/* - factory(\App\Models\ClientLocation::class,1)->create([ - 'client_id' => $c->id, - 'is_primary_billing' => 1 - ]); - factory(\App\Models\ClientLocation::class,10)->create([ - 'client_id' => $c->id, - ]); -*/ }); diff --git a/routes/api.php b/routes/api.php index 3521e200def0..63b1192f8e3c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -17,3 +17,52 @@ Route::middleware('auth:api')->get('/user', function (Request $request) { return $request->user(); }); */ + +Route::group(['middleware' => ['api_secret_check']], function () { + + Route::post('signup', 'AccountController@store')->name('signup.submit'); + +}); + + +Route::group(['middleware' => ['api_secret_check','token_auth']], function () { + + Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit + + Route::resource('invoices', 'InvoiceController'); // name = (invoices. index / create / show / update / destroy / edit + + Route::post('invoices/bulk', 'InvoiceController@bulk')->name('invoices.bulk'); + + Route::resource('quotes', 'QuoteController'); // name = (quotes. index / create / show / update / destroy / edit + + Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk'); + + Route::resource('recurring_invoices', 'RecurringInvoiceController'); // name = (recurring_invoices. index / create / show / update / destroy / edit + + Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk'); + + Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk'); + + Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit + + Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit + + Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); + + Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit + + Route::post('payments/bulk', 'PaymentController@bulk')->name('payments.bulk'); + + Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit + + Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk'); + + Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit + + Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); + + Route::resource('user', 'UserProfileController'); // name = (clients. index / create / show / update / destroy / edit + + Route::get('settings', 'SettingsController@index')->name('user.settings'); + +}); \ No newline at end of file diff --git a/tests/Browser/CreateAccountTest.php b/tests/Browser/CreateAccountTest.php index 8532bc7a48fc..f1785f80b0e6 100644 --- a/tests/Browser/CreateAccountTest.php +++ b/tests/Browser/CreateAccountTest.php @@ -12,7 +12,6 @@ class CreateAccountTest extends DuskTestCase { use WithFaker; - use DatabaseTransactions; public function testSignupFormDisplayed() @@ -27,7 +26,6 @@ class CreateAccountTest extends DuskTestCase */ public function testCreateAValidUser() { - DB::beginTransaction(); $this->browse(function (Browser $browser) { $browser->visit('/signup') @@ -41,7 +39,6 @@ class CreateAccountTest extends DuskTestCase ->assertPathIs('/dashboard'); }); - DB::rollback(); } } diff --git a/tests/Feature/AccountTest.php b/tests/Feature/AccountTest.php new file mode 100644 index 000000000000..28904acef1a5 --- /dev/null +++ b/tests/Feature/AccountTest.php @@ -0,0 +1,83 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testAccountCreation() + { + $data = [ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $this->faker->unique()->safeEmail, + 'password' => 'ALongAndBrilliantPassword123', + '_token' => csrf_token(), + 'privacy_policy' => 1, + 'terms_of_service' => 1 + ]; + + $response = $this->post('/signup', $data); + + $response->assertStatus(200) + ->assertJson([ + 'first_name' => $data['first_name'], + ]); + + } + + public function testApiAccountCreation() + { + + $data = [ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $this->faker->unique()->safeEmail, + 'password' => 'ALongAndBrilliantPassword123', + '_token' => csrf_token(), + 'privacy_policy' => 1, + 'terms_of_service' => 1 + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + ])->post('/api/v1/signup', $data); + + $response->assertStatus(200) + ->assertJson([ + 'first_name' => $data['first_name'], + ]); + + } + + + +} diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php index f2964f32db48..36cd065cf410 100644 --- a/tests/Feature/ClientTest.php +++ b/tests/Feature/ClientTest.php @@ -2,11 +2,8 @@ namespace Tests\Feature; -use App\Jobs\Account\CreateAccount; use App\Models\Account; -use App\Models\Client; -use App\Models\User; -use App\Utils\Traits\UserSessionAttributes; +use App\Models\Company; use Faker\Factory; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -19,19 +16,22 @@ use Tests\TestCase; class ClientTest extends TestCase { - //use DatabaseTransactions; public function setUp() { parent::setUp(); + Session::start(); $this->faker = \Faker\Factory::create(); + Model::reguard(); + } - public function testAccountCreation() + public function testClientList() { + $data = [ 'first_name' => $this->faker->firstName, 'last_name' => $this->faker->lastName, @@ -42,20 +42,48 @@ class ClientTest extends TestCase 'terms_of_service' => 1 ]; - $response = $this->post('/signup', $data); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + ])->post('/api/v1/signup', $data); + $response->assertStatus(200) ->assertJson([ 'first_name' => $data['first_name'], ]); + $acc = $response->json(); + + $account = Account::find($acc['id']); + + $token = $this->account->default_company->tokens()->first()->token; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $token, + ])->get('/api/v1/clients'); + + $response->assertStatus(200); + } - public function testUserCreated() + public function testClientShow() { - $this->assertTrue(true); - } + $account = Account::all()->first(); + $token = $account->default_company->tokens()->first()->token; + $client = $account->default_company->clients()->first(); + + $this->assertEquals(var_dump($account->default_company->clients->first()),1); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $token, + ])->get('/api/v1/clients/'.$client->id); + + $response->assertStatus(200); + } } diff --git a/tests/Feature/LoginTest.php b/tests/Feature/LoginTest.php index 651eddb5680a..acc3ecb3d8a6 100644 --- a/tests/Feature/LoginTest.php +++ b/tests/Feature/LoginTest.php @@ -16,8 +16,9 @@ use Tests\TestCase; class LoginTest extends TestCase { - use DatabaseTransactions; + //use DatabaseTransactions; use UserSessionAttributes; + use RefreshDatabase; public function setUp() { diff --git a/tests/Integration/MultiDBUserTest.php b/tests/Integration/MultiDBUserTest.php index 3c6df4447c73..f6e173447f3f 100644 --- a/tests/Integration/MultiDBUserTest.php +++ b/tests/Integration/MultiDBUserTest.php @@ -22,7 +22,7 @@ use Tests\TestCase; class MultiDBUserTest extends TestCase { //use DatabaseMigrations; - use InteractsWithDatabase; + //use InteractsWithDatabase; public function setUp() { diff --git a/tests/Integration/UniqueEmailTest.php b/tests/Integration/UniqueEmailTest.php index 6290a5017dea..e158a1756f37 100644 --- a/tests/Integration/UniqueEmailTest.php +++ b/tests/Integration/UniqueEmailTest.php @@ -17,7 +17,7 @@ use Tests\TestCase; */ class UniqueEmailTest extends TestCase { - use InteractsWithDatabase; + //use InteractsWithDatabase; protected $rule;