1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-05 18:52:44 +01:00

Fixes for V2 (#3408)

* Refactor for user

* payment notifications

* Fixes for contact request

* Fix validation for contacts

* Fixes for base repo

* Fixes for Invoice Repo

* hide password field on clientcontact
This commit is contained in:
David Bomba 2020-03-02 21:22:37 +11:00 committed by GitHub
parent 36abedf6aa
commit db88d6a50d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 411 additions and 64 deletions

View File

@ -130,7 +130,6 @@ class CreateTestData extends Command
$this->info('Creating '.$this->count. ' clients');
for ($x=0; $x<$this->count; $x++) {
$z = $x+1;
$this->info("Creating client # ".$z);

View File

@ -62,4 +62,15 @@ class SelfUpdateController extends BaseController
return response()->json(['message'=>$res], 200);
}
public function checkVersion(UpdaterManager $updater)
{
//echo $updater->source()->getVersionInstalled();
//echo $updater->source()->isNewVersionAvailable();
//echo $updater->source()->getVersionAvailable();
}
}

View File

@ -439,20 +439,7 @@ class UserController extends BaseController
public function destroy(DestroyUserRequest $request, User $user)
{
/* If the user passes the company user we archive the company user */
if(array_key_exists('company_user', $request->all()))
{
$this->forced_includes = 'company_users';
$company = auth()->user()->company();
$cu = CompanyUser::whereUserId($user->id)
->whereCompanyId($company->id)
->first();
$cu->delete();
}
else
$user->delete();
$user = $this->user_repo->destroy($request->all(), $user);
return $this->itemResponse($user->fresh());
}

View File

@ -37,7 +37,9 @@ 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('X-APP-VERSION', config('ninja.app_version'));
$response->headers->set('X-API-VERSION', config('ninja.api_version'));
return $response;
}
}

View File

@ -16,7 +16,6 @@ use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Models\Client;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
class StoreClientRequest extends Request
@ -43,6 +42,7 @@ class StoreClientRequest extends Request
$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
@ -72,7 +72,6 @@ class StoreClientRequest extends Request
$input = $this->all();
//@todo implement feature permissions for > 100 clients
if (!isset($input['settings'])) {
$input['settings'] = ClientSettings::defaults();
}
@ -94,16 +93,24 @@ class StoreClientRequest extends Request
//Filter the client contact password - if it is sent with ***** we should ignore it!
if(isset($contact['password']))
{
$contact['password'] = str_replace("*", "", $contact['password']);
if(strlen($contact['password']) == 0)
unset($input['contacts'][$key]['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']);
}
}
}
}
}
$this->replace($input);
}

View File

@ -18,7 +18,6 @@ use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Http\ValidationRules\ValidSettingsRule;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
class UpdateClientRequest extends Request
@ -50,6 +49,7 @@ class UpdateClientRequest extends Request
$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
@ -88,18 +88,28 @@ class UpdateClientRequest extends Request
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']))
{
$contact['password'] = str_replace("*", "", $contact['password']);
if(strlen($contact['password']) == 0)
unset($input['contacts'][$key]['password']);
//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']);
}
}
}
}
}
$this->replace($input);
}
}

View File

@ -60,5 +60,7 @@ class PaymentCreatedActivity implements ShouldQueue
if (count($invoices) == 0) {
$this->activityRepo->save($fields, $payment);
}
}
}

View File

@ -0,0 +1,50 @@
<?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\Listeners\Payment;
use App\Models\Activity;
use App\Models\Invoice;
use App\Models\Payment;
use App\Notifications\Payment\NewPaymentNotification;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class PaymentNotification implements ShouldQueue
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$payment = $event->payment;
//$invoices = $payment->invoices;
foreach($payment->company->company_users as $company_user)
{
$company_user->user->notify(new NewPaymentNotification($payment, $payment->company));
}
}
}

View File

@ -143,31 +143,31 @@ class Account extends BaseModel
return $self_host || ! empty($plan_details);
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case FEATURE_MORE_CLIENTS:
case self::FEATURE_MORE_CLIENTS:
return $self_host || ! empty($plan_details) && (! $plan_details['trial'] || ! empty($this->getPlanDetails(false, false)));
// White Label
case FEATURE_WHITE_LABEL:
case self::FEATURE_WHITE_LABEL:
if (! $self_host && $plan_details && ! $plan_details['expires']) {
return false;
}
// Fallthrough
case FEATURE_REMOVE_CREATED_BY:
case self::FEATURE_REMOVE_CREATED_BY:
return ! empty($plan_details); // A plan is required even for self-hosted users
// Enterprise; No Trial allowed; grandfathered for old pro users
case FEATURE_USERS:// Grandfathered for old Pro users
case self::FEATURE_USERS:// Grandfathered for old Pro users
if ($planDetails && $planDetails['trial']) {
// Do they have a non-trial plan?
$planDetails = $this->getPlanDetails(false, false);
}
return $selfHost || ! empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE));
return $selfHost || ! empty($planDetails) && ($planDetails['plan'] == self::PLAN_ENTERPRISE);
// Enterprise; No Trial allowed
case FEATURE_DOCUMENTS:
case FEATURE_USER_PERMISSIONS:
return $selfHost || ! empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && ! $planDetails['trial'];
case self::FEATURE_DOCUMENTS:
case self::FEATURE_USER_PERMISSIONS:
return $selfHost || ! empty($planDetails) && $planDetails['plan'] == self::PLAN_ENTERPRISE && ! $planDetails['trial'];
default:
return false;
@ -181,14 +181,16 @@ class Account extends BaseModel
public function isPaidHostedClient()
{
if (! Ninja::isNinja())
return false;
return $this->plan == 'pro' || $this->plan == 'enterprise';
}
public function isTrial()
{
if (! Ninja::isNinja()) {
if (! Ninja::isNinja())
return false;
}
$plan_details = $this->getPlanDetails();
@ -197,10 +199,7 @@ class Account extends BaseModel
public function getPlanDetails($include_inactive = false, $include_trial = true)
{
if (!$this) {
return null;
}
$plan = $this->plan;
$price = $this->plan_price;
$trial_plan = $this->trial_plan;

View File

@ -0,0 +1,109 @@
<?php
namespace App\Notifications\Payment;
use App\Mail\Signup\NewSignup;
use App\Utils\Number;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class NewPaymentNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
protected $payment;
protected $company;
protected $settings;
public function __construct($payment, $company, $settings = null)
{
$this->payment = $payment;
$this->company = $company;
$this->settings = $payment->client->getMergedSettings();
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
//return ['mail'];
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$amount = Number::formatMoney($this->payment->amount, $this->payment->client);
$invoice_texts = ctrans('texts.invoice_number_short');
foreach($this->payment->invoices as $invoice)
{
$invoice_texts .= $invoice->number . ',';
}
$invoice_texts = rtrim($invoice_texts);
$data = [
'title' => ctrans('texts.notification_payment_paid_subject', ['client' => $this->payment->client->present()->name()]),
'message' => ctrans('texts.notification_paid_paid', ['amount' => $amount, 'client' => $this->payment->client->present()->name(), 'invoice' => $invoice_texts]),
'url' => config('ninja.site_url') . '/payments/' . $this->payment->hashed_id,
'button' => ctrans('texts.view_payment'),
'signature' => $this->company->settings->email_signature,
'logo' => $this->company->present()->logo(),
];
return (new MailMessage)
->subject(ctrans('texts.notification_payment_paid_subject', ['client' => $this->payment->client->present()->name()]))
->markdown('email.admin.generic', $data);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$logo = $this->company->present()->logo();
return (new SlackMessage)
->success()
->to("#devv2")
->from("System")
->image($logo)
->content("A new account has been created by {$user_name} - {$email} - from IP: {$ip}");
}
}

View File

@ -38,6 +38,7 @@ use App\Listeners\Invoice\InvoiceEmailActivity;
use App\Listeners\Invoice\InvoiceEmailFailedActivity;
use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Invoice\UpdateInvoiceInvitations;
use App\Listeners\Payment\PaymentNotification;
use App\Listeners\SendVerificationNotification;
use App\Listeners\SetDBListener;
use App\Listeners\User\UpdateUserLastLogin;
@ -73,6 +74,7 @@ class EventServiceProvider extends ServiceProvider
],
PaymentWasCreated::class => [
PaymentCreatedActivity::class,
PaymentNotification::class,
],
PaymentWasDeleted::class => [
PaymentDeletedActivity::class,

View File

@ -16,8 +16,10 @@ use App\Factory\QuoteInvitationFactory;
use App\Jobs\Product\UpdateOrCreateProduct;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Quote;
use App\Utils\Traits\MakesHash;
use ReflectionClass;
@ -166,9 +168,17 @@ class BaseRepository
return $this->getInstance()->scope($ids)->withTrashed()->get();
}
public function getInvitationByKey($key)
public function getInvitation($invitation, $resource)
{
return InvoiceInvitation::whereRaw("BINARY `key`= ?", [$key])->first();
if(!array_key_exists('key', $invitation))
return false;
$invitation_class = sprintf("App\\Models\\%sInvitation", $resource);
$invitation = $invitation_class::whereRaw("BINARY `key`= ?", [$invitation['key']])->first();
return $invitation;
}
/**
@ -177,6 +187,7 @@ class BaseRepository
protected function alternativeSave($data, $model)
{
$class = new ReflectionClass($model);
$state = [];
$resource = explode('\\', $class->name)[2]; /** This will extract 'Invoice' from App\Models\Invoice */
$lcfirst_resource_id = lcfirst($resource) . '_id';
@ -209,21 +220,13 @@ class BaseRepository
/* Get array of Keys which have been removed from the invitations array and soft delete each invitation */
$model->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) {
$this->getInvitationByKey($invitation)->delete();
$this->getInvitation($invitation, $resource)->delete();
});
foreach ($data['invitations'] as $invitation) {
$inv = false;
if (array_key_exists('key', $invitation)) {
$inv = $this->getInvitationByKey([$invitation['key']]);
if($inv)
$inv->forceDelete();
}
if (!$inv) {
//if no invitations are present - create one.
if (! $this->getInvitation($invitation, $resource) ) {
if (isset($invitation['id'])) {
unset($invitation['id']);

View File

@ -15,6 +15,7 @@ use App\Factory\ClientContactFactory;
use App\Models\Client;
use App\Models\ClientContact;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
/**
* ClientContactRepository
@ -54,8 +55,23 @@ class ClientContactRepository extends BaseRepository
}
$update_contact->fill($contact);
if(array_key_exists('password', $contact)) {
if(strlen($contact['password']) == 0){
$update_contact->password = '';
}
else{
$update_contact->password = Hash::make($contact['password']);
}
}
$update_contact->save();
});

View File

@ -58,4 +58,9 @@ class InvoiceRepository extends BaseRepository {
public function markSent(Invoice $invoice):?Invoice {
return $invoice->service()->markSent()->save();
}
public function getInvitationByKey($key) :?InvoiceInvitation
{
return InvoiceInvitation::whereRaw("BINARY `key`= ?", [$key])->first();
}
}

View File

@ -52,7 +52,7 @@ class UserRepository extends BaseRepository
$company = auth()->user()->company();
$account_id = $company->account->id;
$cu = CompanyUser::whereUserId($user->id)->whereCompanyId($company->id)->first();
$cu = CompanyUser::whereUserId($user->id)->whereCompanyId($company->id)->withTrashed()->first();
/*No company user exists - attach the user*/
if (!$cu) {
@ -60,10 +60,34 @@ class UserRepository extends BaseRepository
$user->companies()->attach($company->id, $data['company_user']);
} else {
$cu->fill($data['company_user']);
$cu->restore();
$cu->tokens()->restore();
$cu->save();
}
}
return $user;
}
public function destroy(array $data, User $user)
{
if(array_key_exists('company_user', $data))
{
$this->forced_includes = 'company_users';
$company = auth()->user()->company();
$cu = CompanyUser::whereUserId($user->id)
->whereCompanyId($company->id)
->first();
$cu->tokens()->delete();
$cu->delete();
}
else
$user->delete();
return $user->fresh();
}
}

View File

@ -121,6 +121,8 @@ class CreditTransformer extends EntityTransformer
'custom_surcharge_taxes' => (bool) $credit->custom_surcharge_taxes,
'line_items' => $credit->line_items ?: (array)[],
'backup' => $credit->backup ?: '',
'entity_type' => 'credit',
];
}
}

View File

@ -130,6 +130,7 @@ class InvoiceTransformer extends EntityTransformer
'custom_surcharge_taxes' => (bool) $invoice->custom_surcharge_taxes,
'line_items' => $invoice->line_items ?: (array)[],
'backup' => $invoice->backup ?: '',
'entity_type' => 'invoice',
];
}
}

View File

@ -121,6 +121,8 @@ class QuoteTransformer extends EntityTransformer
'custom_surcharge_taxes' => (bool) $quote->custom_surcharge_taxes,
'line_items' => $quote->line_items ?: (array)[],
'backup' => $quote->backup ?: '',
'entity_type' => 'quote',
];
}
}

View File

@ -3119,6 +3119,9 @@ $LANG = array(
'new_signup' => 'New Signup',
'new_signup_text' => 'A new account has been created by :user - :email - from IP address: :ip',
'notification_payment_paid_subject' => 'Payment was made by :client',
'notification_paid_paid' => 'A payment of :amount was made by client :client towards :invoice.',
'email_link_not_working' => 'If button above isn\'t working for you, please click on the link',
);

View File

@ -119,6 +119,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('self-update', 'SelfUpdateController@update')->middleware('password_protected');
Route::post('self-update/check_version', 'SelfUpdateController@checkVersion')->middleware('password_protected');
/*
Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit

View File

@ -42,7 +42,8 @@ class ClientTest extends TestCase
$this->withoutExceptionHandling();
Client::reguard();
ClientContact::reguard();
}
public function testClientList()
@ -362,7 +363,7 @@ class ClientTest extends TestCase
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
\Log::error($message);
//\Log::error($message);
$this->assertNotNull($message);
}
@ -387,7 +388,7 @@ class ClientTest extends TestCase
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
//\Log::error($message);
////\Log::error($message);
//$this->assertNotNull($message);
}
@ -418,7 +419,7 @@ class ClientTest extends TestCase
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
\Log::error($message);
//\Log::error($message);
$this->assertNotNull($message);
}
@ -437,8 +438,118 @@ class ClientTest extends TestCase
$arr = $response->json();
//\Log::error($arr);
$safe_email = $this->faker->unique()->safeEmail;
$data = [
'name' => 'A loyal Client',
'contacts' => [
[
'email' => $safe_email,
'password' => ''
],
]
];
$response = null;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->post('/api/v1/clients/', $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
//\Log::error($message);
$this->assertNotNull($message);
}
$response->assertStatus(200);
$arr = $response->json();
$client = Client::find($this->decodePrimaryKey($arr['data']['id']));
$contact = $client->contacts()->whereEmail($safe_email)->first();
$this->assertEquals(0, strlen($contact->password));
$safe_email = $this->faker->unique()->safeEmail;
$data = [
'name' => 'A loyal Client',
'contacts' => [
[
'email' => $safe_email,
'password' => 'AFancyDancy191$Password'
],
]
];
$response = null;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->post('/api/v1/clients/', $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
//\Log::error($message);
$this->assertNotNull($message);
}
$response->assertStatus(200);
$arr = $response->json();
$client = Client::find($this->decodePrimaryKey($arr['data']['id']));
$contact = $client->contacts()->whereEmail($safe_email)->first();
$this->assertGreaterThan(1, strlen($contact->password));
$password = $contact->password;
$data = [
'name' => 'A Stary eyed client',
'contacts' => [
[
'id' => $contact->hashed_id,
'email' => $safe_email,
'password' => '*****'
],
]
];
$response = null;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->put('/api/v1/clients/' . $client->hashed_id, $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
//\Log::error($message);
$this->assertNotNull($message);
}
$response->assertStatus(200);
$arr = $response->json();
$client = Client::find($this->decodePrimaryKey($arr['data']['id']));
$client->fresh();
$contact = $client->contacts()->whereEmail($safe_email)->first();
$this->assertEquals($password, $contact->password);
}
/** @test */
// public function testMassivelyCreatingClients()