Minors Fixes (#3405)

* Tests for client contact passwords

* test for client API

* Client Tests for password quality

* Final tests for client contact password

* Implement feature permissions

* Minor fixes
This commit is contained in:
David Bomba 2020-03-01 21:18:13 +11:00 committed by GitHub
parent e2ed1fad8b
commit 0ff14c97fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 357 additions and 9 deletions

View File

@ -146,10 +146,12 @@ class CompanySettings extends BaseSettings {
public $email_subject_invoice = '';
public $email_subject_quote = '';
public $email_subject_payment = '';
public $email_subject_payment_partial = '';
public $email_subject_statement = '';
public $email_template_invoice = '';
public $email_template_quote = '';
public $email_template_payment = '';
public $email_template_payment_partial = '';
public $email_template_statement = '';
public $email_subject_reminder1 = '';
public $email_subject_reminder2 = '';
@ -311,9 +313,11 @@ class CompanySettings extends BaseSettings {
'email_subject_invoice' => 'string',
'email_subject_quote' => 'string',
'email_subject_payment' => 'string',
'email_subject_payment_partial' => 'string',
'email_template_invoice' => 'string',
'email_template_quote' => 'string',
'email_template_payment' => 'string',
'email_template_payment_partial' => 'string',
'email_subject_reminder1' => 'string',
'email_subject_reminder2' => 'string',
'email_subject_reminder3' => 'string',

View File

@ -142,6 +142,7 @@ class CompanyUserController extends BaseController
}
else {
$company_user->fill($request->input('company_user')['settings']);
$company_user->fill($request->input('company_user')['notifications']);
}
$company_user->save();

View File

@ -11,7 +11,9 @@
namespace App\Http\Middleware;
use App\Models\Account;
use App\Models\Language;
use App\Utils\CurlUtils;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
@ -39,14 +41,15 @@ class StartupCheck
// $start = microtime(true);
// Log::error('start up check');
$cached_tables = config('ninja.cached_tables');
if (Input::has('clear_cache')) {
if ($request->has('clear_cache')) {
Session::flash('message', 'Cache cleared');
}
/* Make sure our cache is built */
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (Input::has('clear_cache') || ! Cache::has($name)) {
if ($request->has('clear_cache') || ! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
@ -67,6 +70,69 @@ class StartupCheck
}
}
/* Catch claim license requests */
if(config('ninja.environment') == 'selfhost' && $request->has('license_key') && $request->has('product_id') && $request->segment(3) == 'claim_license')
{
$license_key = $request->input('license_key');
$product_id = $request->input('product_id');
$url = config('ninja.license_url') . "/claim_license?license_key={$license_key}&product_id={$product_id}&get_date=true";
$data = trim(CurlUtils::get($url));
if ($data == Account::RESULT_FAILURE) {
$error = [
'message' => trans('texts.invalid_white_label_license'),
'errors' => []
];
return response()->json($error, 400);
} elseif ($data) {
$date = date_create($data)->modify('+1 year');
if ($date < date_create()) {
$error = [
'message' => trans('texts.invalid_white_label_license'),
'errors' => []
];
return response()->json($error, 400);
} else {
$account = auth()->user()->company()->account;
$account->plan_term = Account::PLAN_TERM_YEARLY;
$account->plan_paid = $data;
$account->plan_expires = $date->format('Y-m-d');
$account->plan = Account::PLAN_WHITE_LABEL;
$account->save();
$error = [
'message' => trans('texts.bought_white_label'),
'errors' => []
];
return response()->json($error, 200);
}
} else {
$error = [
'message' => trans('texts.white_label_license_error'),
'errors' => []
];
return response()->json($error, 400);
}
}
$response = $next($request);
return $response;

View File

@ -68,8 +68,11 @@ class StoreClientRequest extends Request
protected function prepareForValidation()
{
$input = $this->all();
//@todo implement feature permissions for > 100 clients
if (!isset($input['settings'])) {
$input['settings'] = ClientSettings::defaults();
}

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Laracasts\Presenter\PresentableTrait;
@ -52,6 +53,37 @@ class Account extends BaseModel
'discount_expires',
];
const PLAN_FREE = 'free';
const PLAN_PRO = 'pro';
const PLAN_ENTERPRISE = 'enterprise';
const PLAN_WHITE_LABEL = 'white_label';
const PLAN_TERM_MONTHLY = 'month';
const PLAN_TERM_YEARLY = 'year';
const FEATURE_TASKS = 'tasks';
const FEATURE_EXPENSES = 'expenses';
const FEATURE_QUOTES = 'quotes';
const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs';
const FEATURE_DIFFERENT_DESIGNS = 'different_designs';
const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders';
const FEATURE_INVOICE_SETTINGS = 'invoice_settings';
const FEATURE_CUSTOM_EMAILS = 'custom_emails';
const FEATURE_PDF_ATTACHMENT = 'pdf_attachments';
const FEATURE_MORE_INVOICE_DESIGNS = 'more_invoice_designs';
const FEATURE_REPORTS = 'reports';
const FEATURE_BUY_NOW_BUTTONS = 'buy_now_buttons';
const FEATURE_API = 'api';
const FEATURE_CLIENT_PORTAL_PASSWORD = 'client_portal_password';
const FEATURE_CUSTOM_URL = 'custom_url';
const FEATURE_MORE_CLIENTS = 'more_clients';
const FEATURE_WHITE_LABEL = 'white_label';
const FEATURE_REMOVE_CREATED_BY = 'remove_created_by';
const FEATURE_USERS = 'users'; // Grandfathered for old Pro users
const FEATURE_DOCUMENTS = 'documents';
const FEATURE_USER_PERMISSIONS = 'permissions';
const RESULT_FAILURE = 'failure';
const RESULT_SUCCESS = 'success';
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -82,4 +114,180 @@ class Account extends BaseModel
{
return $this->plan ?: '';
}
public function hasFeature($feature)
{
$plan_details = $this->getPlanDetails();
$self_host = ! Ninja::isNinja();
switch ($feature) {
case self::FEATURE_TASKS:
case self::FEATURE_EXPENSES:
case self::FEATURE_QUOTES:
return true;
case self::FEATURE_CUSTOMIZE_INVOICE_DESIGN:
case self::FEATURE_DIFFERENT_DESIGNS:
case self::FEATURE_EMAIL_TEMPLATES_REMINDERS:
case self::FEATURE_INVOICE_SETTINGS:
case self::FEATURE_CUSTOM_EMAILS:
case self::FEATURE_PDF_ATTACHMENT:
case self::FEATURE_MORE_INVOICE_DESIGNS:
case self::FEATURE_REPORTS:
case self::FEATURE_BUY_NOW_BUTTONS:
case self::FEATURE_API:
case self::FEATURE_CLIENT_PORTAL_PASSWORD:
case self::FEATURE_CUSTOM_URL:
return $self_host || ! empty($plan_details);
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case FEATURE_MORE_CLIENTS:
return $self_host || ! empty($plan_details) && (! $plan_details['trial'] || ! empty($this->getPlanDetails(false, false)));
// White Label
case FEATURE_WHITE_LABEL:
if (! $self_host && $plan_details && ! $plan_details['expires']) {
return false;
}
// Fallthrough
case 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
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));
// Enterprise; No Trial allowed
case FEATURE_DOCUMENTS:
case FEATURE_USER_PERMISSIONS:
return $selfHost || ! empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && ! $planDetails['trial'];
default:
return false;
}
}
public function isPaid()
{
return Ninja::isNinja() ? ($this->isPaidHostedClient() && ! $this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
}
public function isPaidHostedClient()
{
return $this->plan == 'pro' || $this->plan == 'enterprise';
}
public function isTrial()
{
if (! Ninja::isNinja()) {
return false;
}
$plan_details = $this->getPlanDetails();
return $plan_details && $plan_details['trial'];
}
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;
if ((! $plan || $plan == self::PLAN_FREE) && (! $trial_plan || ! $include_trial)) {
return null;
}
$trial_active = false;
if ($trial_plan && $include_trial) {
$trial_started = \DateTime::createFromFormat('Y-m-d', $this->trial_started);
$trial_expires = clone $trial_started;
$trial_expires->modify('+2 weeks');
if ($trial_expires >= date_create()) {
$trial_active = true;
}
}
$plan_active = false;
if ($plan) {
if ($this->plan_expires == null) {
$plan_active = true;
$plan_expires = false;
} else {
$plan_expires = \DateTime::createFromFormat('Y-m-d', $this->plan_expires);
if ($plan_expires >= date_create()) {
$plan_active = true;
}
}
}
if (! $include_inactive && ! $plan_active && ! $trial_active) {
return null;
}
// Should we show plan details or trial details?
if (($plan && ! $trial_plan) || ! $include_trial) {
$use_plan = true;
} elseif (! $plan && $trial_plan) {
$use_plan = false;
} else {
// There is both a plan and a trial
if (! empty($plan_active) && empty($trial_active)) {
$use_plan = true;
} elseif (empty($plan_active) && ! empty($trial_active)) {
$use_plan = false;
} elseif (! empty($plan_active) && ! empty($trial_active)) {
// Both are active; use whichever is a better plan
if ($plan == self::PLAN_ENTERPRISE) {
$use_plan = true;
} elseif ($trial_plan == self::PLAN_ENTERPRISE) {
$use_plan = false;
} else {
// They're both the same; show the plan
$use_plan = true;
}
} else {
// Neither are active; use whichever expired most recently
$use_plan = $plan_expires >= $trial_expires;
}
}
if ($use_plan) {
return [
'account_id' => $this->id,
'num_users' => $this->num_users,
'plan_price' => $price,
'trial' => false,
'plan' => $plan,
'started' => \DateTime::createFromFormat('Y-m-d', $this->plan_started),
'expires' => $plan_expires,
'paid' => \DateTime::createFromFormat('Y-m-d', $this->plan_paid),
'term' => $this->plan_term,
'active' => $plan_active,
];
} else {
return [
'account_id' => $this->id,
'num_users' => 1,
'plan_price' => 0,
'trial' => true,
'plan' => $trial_plan,
'started' => $trial_started,
'expires' => $trial_expires,
'active' => $trial_active,
];
}
}
}

View File

@ -12,17 +12,18 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\SoftDeletes;
class CompanyUser extends Pivot
{
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
use SoftDeletes;
// protected $guarded = ['id'];
protected $dateFormat = 'Y-m-d H:i:s.u';
/**
* The attributes that should be cast to native types.
*
@ -38,6 +39,7 @@ class CompanyUser extends Pivot
protected $fillable = [
'account_id',
'permissions',
'notifications',
'settings',
'is_admin',
'is_owner',

View File

@ -47,7 +47,7 @@ class ClientContactTransformer extends EntityTransformer
'contact_key' => $contact->contact_key ?: '',
'send_email' => (bool) $contact->send_email,
'last_login' => (int)$contact->last_login,
'password' => '',
'password' => isset($contact->password) ? '*****' : '',
];
}
}

53
app/Utils/CurlUtils.php Normal file
View File

@ -0,0 +1,53 @@
<?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\Utils;
class CurlUtils
{
public static function post($url, $data, $headers = false)
{
return self::exec('POST', $url, $data, $headers);
}
public static function get($url, $headers = false)
{
return self::exec('GET', $url, null, $headers);
}
public static function exec($method, $url, $data, $headers = false)
{
$curl = curl_init();
$opts = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => $method,
CURLOPT_HTTPHEADER => $headers ?: [],
];
if ($data) {
$opts[CURLOPT_POSTFIELDS] = $data;
}
curl_setopt_array($curl, $opts);
$response = curl_exec($curl);
if ($error = curl_error($curl)) {
\Log::error('CURL Error #' . curl_errno($curl) . ': ' . $error);
}
curl_close($curl);
return $response;
}
}

View File

@ -28,6 +28,11 @@ class Ninja
return config('ninja.environment') === 'hosted';
}
public static function isNinja()
{
return config('ninja.production');
}
public static function getDebugInfo()
{
$mysql_version = DB::select(DB::raw("select version() as version"))[0]->version;

View File

@ -3,6 +3,8 @@
return [
'web_url' => 'https://www.invoiceninja.com',
'license_url' => 'https://app.invoiceninja.com',
'production' => env('NINJA_PROD', false),
'app_name' => env('APP_NAME'),
'site_url' => env('APP_URL', ''),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),

View File

@ -198,10 +198,13 @@ class CreateUsersTable extends Migration
$table->unsignedInteger('account_id');
$table->unsignedInteger('user_id')->index();
$table->mediumText('permissions')->nullable();
$table->mediumText('notifications')->nullable();
$table->mediumText('settings')->nullable();
$table->boolean('is_owner')->default(false);
$table->boolean('is_admin')->default(false);
$table->boolean('is_locked')->default(false); // locks user out of account
$table->softDeletes('deleted_at', 6);
$table->timestamps(6);
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');

View File

@ -436,7 +436,8 @@ class ClientTest extends TestCase
$arr = $response->json();
\Log::error($arr);
//\Log::error($arr);
}
/** @test */