mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-11 21:52:35 +01:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
6a7cfdeb64
57
app/Console/Commands/PruneData.php
Normal file
57
app/Console/Commands/PruneData.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php namespace App\Console\Commands;
|
||||
|
||||
use DB;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PruneData extends Command
|
||||
{
|
||||
protected $name = 'ninja:prune-data';
|
||||
protected $description = 'Delete inactive accounts';
|
||||
|
||||
public function fire()
|
||||
{
|
||||
$this->info(date('Y-m-d').' Running PruneData...');
|
||||
|
||||
// delete accounts who never registered, didn't create any invoices,
|
||||
// hansn't logged in within the past 6 months and isn't linked to another account
|
||||
$sql = 'select a.id
|
||||
from (select id, last_login from accounts) a
|
||||
left join users u on u.account_id = a.id and u.public_id = 0
|
||||
left join invoices i on i.account_id = a.id
|
||||
left join user_accounts ua1 on ua1.user_id1 = u.id
|
||||
left join user_accounts ua2 on ua2.user_id2 = u.id
|
||||
left join user_accounts ua3 on ua3.user_id3 = u.id
|
||||
left join user_accounts ua4 on ua4.user_id4 = u.id
|
||||
left join user_accounts ua5 on ua5.user_id5 = u.id
|
||||
where u.registered = 0
|
||||
and a.last_login < DATE_SUB(now(), INTERVAL 6 MONTH)
|
||||
and (ua1.id is null and ua2.id is null and ua3.id is null and ua4.id is null and ua5.id is null)
|
||||
group by a.id
|
||||
having count(i.id) = 0';
|
||||
|
||||
$results = DB::select($sql);
|
||||
|
||||
foreach ($results as $result) {
|
||||
$this->info("Deleting {$result->id}");
|
||||
DB::table('accounts')
|
||||
->where('id', '=', $result->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
$this->info('Done');
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
{
|
||||
return array(
|
||||
//array('example', InputArgument::REQUIRED, 'An example argument.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
return array(
|
||||
//array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null),
|
||||
);
|
||||
}
|
||||
}
|
@ -62,8 +62,12 @@ class SendRenewalInvoices extends Command
|
||||
$invoice->due_date = date('Y-m-d', strtotime('+ 10 days'));
|
||||
$invoice->save();
|
||||
|
||||
if ($term == PLAN_TERM_YEARLY) {
|
||||
$this->mailer->sendInvoice($invoice);
|
||||
$this->info("Sent invoice to {$client->getDisplayName()}");
|
||||
$this->info("Sent {$term}ly {$plan} invoice to {$client->getDisplayName()}");
|
||||
} else {
|
||||
$this->info("Created {$term}ly {$plan} invoice for {$client->getDisplayName()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Done');
|
||||
|
@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel
|
||||
'App\Console\Commands\RemoveOrphanedDocuments',
|
||||
'App\Console\Commands\ResetData',
|
||||
'App\Console\Commands\CheckData',
|
||||
'App\Console\Commands\PruneData',
|
||||
'App\Console\Commands\SendRenewalInvoices',
|
||||
'App\Console\Commands\ChargeRenewalInvoices',
|
||||
'App\Console\Commands\SendReminders',
|
||||
|
@ -165,7 +165,7 @@ class AccountController extends BaseController
|
||||
$account->company->save();
|
||||
Session::flash('message', trans('texts.updated_plan'));
|
||||
}
|
||||
} else {
|
||||
} elseif (!empty($planDetails['started'])) {
|
||||
// Downgrade
|
||||
$refund_deadline = clone $planDetails['started'];
|
||||
$refund_deadline->modify('+30 days');
|
||||
@ -186,7 +186,7 @@ class AccountController extends BaseController
|
||||
$gateway = $this->paymentService->createGateway($payment->account_gateway);
|
||||
$refund = $gateway->refund(array(
|
||||
'transactionReference' => $payment->transaction_reference,
|
||||
'amount' => $payment->amount * 100
|
||||
'amount' => $payment->amount
|
||||
));
|
||||
$refund->send();
|
||||
$payment->delete();
|
||||
@ -238,7 +238,7 @@ class AccountController extends BaseController
|
||||
|
||||
if (!empty($new_plan)) {
|
||||
$invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly));
|
||||
return Redirect::to('payment/'.$invitation->invitation_key);
|
||||
return Redirect::to('view/'.$invitation->invitation_key);
|
||||
}
|
||||
|
||||
return Redirect::to('/settings/'.ACCOUNT_MANAGEMENT, 301);
|
||||
|
@ -142,7 +142,9 @@ class StartupCheck
|
||||
} elseif ($productId == PRODUCT_WHITE_LABEL) {
|
||||
if ($data == 'valid') {
|
||||
$company = Auth::user()->account->company;
|
||||
$company->plan_term = PLAN_TERM_YEARLY;
|
||||
$company->plan_paid = date_create()->format('Y-m-d');
|
||||
$company->plan_expires = date_create()->modify('+1 year')->format('Y-m-d');
|
||||
$company->plan = PLAN_WHITE_LABEL;
|
||||
$company->save();
|
||||
|
||||
|
@ -554,6 +554,7 @@ if (!defined('CONTACT_EMAIL')) {
|
||||
define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG');
|
||||
define('NINJA_WEB_URL', 'https://www.invoiceninja.com');
|
||||
define('NINJA_APP_URL', 'https://app.invoiceninja.com');
|
||||
define('NINJA_DATE', '2000-01-01');
|
||||
define('NINJA_VERSION', '2.5.1.3');
|
||||
|
||||
define('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja');
|
||||
|
@ -60,13 +60,7 @@ class AccountGateway extends EntityModel
|
||||
|
||||
public function getConfigField($field)
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
|
||||
if (!$field || !property_exists($config, $field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $config->$field;
|
||||
return object_get($this->getConfig(), $field, false);
|
||||
}
|
||||
|
||||
public function getPublishableStripeKey()
|
||||
|
@ -33,17 +33,17 @@ class InvoiceService extends BaseService
|
||||
public function save($data, $checkSubPermissions = false)
|
||||
{
|
||||
if (isset($data['client'])) {
|
||||
$can_save_client = !$checkSubPermissions;
|
||||
if(!$can_save_client){
|
||||
if(empty($data['client']['public_id']) || $data['client']['public_id']=='-1'){
|
||||
$can_save_client = Client::canCreate();
|
||||
}
|
||||
else{
|
||||
$can_save_client = Client::wherePublicId($data['client']['public_id'])->first()->canEdit();
|
||||
$canSaveClient = !$checkSubPermissions;
|
||||
if( ! $canSaveClient){
|
||||
$clientPublicId = array_get($data, 'client.public_id') ?: array_get($data, 'client.id');
|
||||
if (empty($clientPublicId) || $clientPublicId == '-1') {
|
||||
$canSaveClient = Client::canCreate();
|
||||
} else {
|
||||
$canSaveClient = Client::scope($clientPublicId)->first()->canEdit();
|
||||
}
|
||||
}
|
||||
|
||||
if($can_save_client){
|
||||
if ($canSaveClient) {
|
||||
$client = $this->clientRepo->save($data['client']);
|
||||
$data['client_id'] = $client->id;
|
||||
}
|
||||
|
@ -98,9 +98,13 @@ class PaymentService extends BaseService
|
||||
'number' => isset($input['card_number']) ? $input['card_number'] : null,
|
||||
'expiryMonth' => isset($input['expiration_month']) ? $input['expiration_month'] : null,
|
||||
'expiryYear' => isset($input['expiration_year']) ? $input['expiration_year'] : null,
|
||||
'cvv' => isset($input['cvv']) ? $input['cvv'] : '',
|
||||
];
|
||||
|
||||
// allow space until there's a setting to disable
|
||||
if (isset($input['cvv']) && $input['cvv'] != ' ') {
|
||||
$data['cvv'] = $input['cvv'];
|
||||
}
|
||||
|
||||
if (isset($input['country_id'])) {
|
||||
$country = Country::find($input['country_id']);
|
||||
|
||||
|
@ -13,7 +13,14 @@ class EnterprisePlan extends Migration
|
||||
* @return void
|
||||
*/
|
||||
public function up() {
|
||||
$timeout = ini_get('max_execution_time');
|
||||
if ($timeout == 0) {
|
||||
$timeout = 600;
|
||||
}
|
||||
$timeout = max($timeout - 10, $timeout * .9);
|
||||
$startTime = time();
|
||||
|
||||
if (!Schema::hasTable('companies')) {
|
||||
Schema::create('companies', function($table)
|
||||
{
|
||||
$table->increments('id');
|
||||
@ -36,13 +43,15 @@ class EnterprisePlan extends Migration
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('accounts', 'company_id')) {
|
||||
Schema::table('accounts', function($table)
|
||||
{
|
||||
$table->unsignedInteger('company_id')->nullable();
|
||||
$table->foreign('company_id')->references('id')->on('companies');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$single_account_ids = \DB::table('users')
|
||||
->leftJoin('user_accounts', function ($join) {
|
||||
@ -52,41 +61,65 @@ class EnterprisePlan extends Migration
|
||||
$join->orOn('user_accounts.user_id4', '=', 'users.id');
|
||||
$join->orOn('user_accounts.user_id5', '=', 'users.id');
|
||||
})
|
||||
->leftJoin('accounts', 'accounts.id', '=', 'users.account_id')
|
||||
->whereNull('user_accounts.id')
|
||||
->whereNull('accounts.company_id')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('users.public_id');
|
||||
$query->orWhere('users.public_id', '=', 0);
|
||||
})
|
||||
->lists('users.account_id');
|
||||
|
||||
$group_accounts = \DB::select(
|
||||
'SELECT u1.account_id as account1, u2.account_id as account2, u3.account_id as account3, u4.account_id as account4, u5.account_id as account5 FROM `user_accounts`
|
||||
LEFT JOIN users u1 ON (u1.public_id IS NULL OR u1.public_id = 0) AND user_accounts.user_id1 = u1.id
|
||||
LEFT JOIN users u2 ON (u2.public_id IS NULL OR u2.public_id = 0) AND user_accounts.user_id2 = u2.id
|
||||
LEFT JOIN users u3 ON (u3.public_id IS NULL OR u3.public_id = 0) AND user_accounts.user_id3 = u3.id
|
||||
LEFT JOIN users u4 ON (u4.public_id IS NULL OR u4.public_id = 0) AND user_accounts.user_id4 = u4.id
|
||||
LEFT JOIN users u5 ON (u5.public_id IS NULL OR u5.public_id = 0) AND user_accounts.user_id5 = u5.id');
|
||||
|
||||
if (count($single_account_ids)) {
|
||||
foreach (Account::find($single_account_ids) as $account) {
|
||||
$this->upAccounts($account);
|
||||
$this->checkTimeout($timeout, $startTime);
|
||||
}
|
||||
}
|
||||
|
||||
$group_accounts = \DB::select(
|
||||
'SELECT u1.account_id as account1, u2.account_id as account2, u3.account_id as account3, u4.account_id as account4, u5.account_id as account5 FROM `user_accounts`
|
||||
LEFT JOIN users u1 ON (u1.public_id IS NULL OR u1.public_id = 0) AND user_accounts.user_id1 = u1.id
|
||||
LEFT JOIN users u2 ON (u2.public_id IS NULL OR u2.public_id = 0) AND user_accounts.user_id2 = u2.id
|
||||
LEFT JOIN users u3 ON (u3.public_id IS NULL OR u3.public_id = 0) AND user_accounts.user_id3 = u3.id
|
||||
LEFT JOIN users u4 ON (u4.public_id IS NULL OR u4.public_id = 0) AND user_accounts.user_id4 = u4.id
|
||||
LEFT JOIN users u5 ON (u5.public_id IS NULL OR u5.public_id = 0) AND user_accounts.user_id5 = u5.id
|
||||
LEFT JOIN accounts a1 ON a1.id = u1.account_id
|
||||
LEFT JOIN accounts a2 ON a2.id = u2.account_id
|
||||
LEFT JOIN accounts a3 ON a3.id = u3.account_id
|
||||
LEFT JOIN accounts a4 ON a4.id = u4.account_id
|
||||
LEFT JOIN accounts a5 ON a5.id = u5.account_id
|
||||
WHERE (a1.id IS NOT NULL AND a1.company_id IS NULL)
|
||||
OR (a2.id IS NOT NULL AND a2.company_id IS NULL)
|
||||
OR (a3.id IS NOT NULL AND a3.company_id IS NULL)
|
||||
OR (a4.id IS NOT NULL AND a4.company_id IS NULL)
|
||||
OR (a5.id IS NOT NULL AND a5.company_id IS NULL)');
|
||||
|
||||
if (count($group_accounts)) {
|
||||
foreach ($group_accounts as $group_account) {
|
||||
$this->upAccounts(null, Account::find(get_object_vars($group_account)));
|
||||
$this->checkTimeout($timeout, $startTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('accounts', 'pro_plan_paid')) {
|
||||
Schema::table('accounts', function($table)
|
||||
{
|
||||
$table->dropColumn('pro_plan_paid');
|
||||
$table->dropColumn('pro_plan_trial');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function upAccounts($primaryAccount, $otherAccounts = array()) {
|
||||
if(!$primaryAccount) {
|
||||
$primaryAccount = $otherAccounts->first();
|
||||
}
|
||||
|
||||
if (empty($primaryAccount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$company = Company::create();
|
||||
if ($primaryAccount->pro_plan_paid && $primaryAccount->pro_plan_paid != '0000-00-00') {
|
||||
$company->plan = 'pro';
|
||||
@ -94,13 +127,24 @@ LEFT JOIN users u5 ON (u5.public_id IS NULL OR u5.public_id = 0) AND user_accoun
|
||||
$company->plan_started = $primaryAccount->pro_plan_paid;
|
||||
$company->plan_paid = $primaryAccount->pro_plan_paid;
|
||||
|
||||
if (!Utils::isNinjaProd()) {
|
||||
$company->plan = 'white_label';
|
||||
$company->plan_term = null;
|
||||
} elseif ($company->plan_paid != '2000-01-01'/* NINJA_DATE*/) {
|
||||
$expires = DateTime::createFromFormat('Y-m-d', $primaryAccount->pro_plan_paid);
|
||||
$expires->modify('+1 year');
|
||||
$company->plan_expires = $expires->format('Y-m-d');
|
||||
$expires = $expires->format('Y-m-d');
|
||||
|
||||
// check for self host white label licenses
|
||||
if (!Utils::isNinjaProd()) {
|
||||
if ($company->plan_paid) {
|
||||
$company->plan = 'white_label';
|
||||
// old ones were unlimited, new ones are yearly
|
||||
if ($company->plan_paid == NINJA_DATE) {
|
||||
$company->plan_term = null;
|
||||
} else {
|
||||
$company->plan_term = PLAN_TERM_YEARLY;
|
||||
$company->plan_expires = $expires;
|
||||
}
|
||||
}
|
||||
} elseif ($company->plan_paid != NINJA_DATE) {
|
||||
$company->plan_expires = $expires;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +168,12 @@ LEFT JOIN users u5 ON (u5.public_id IS NULL OR u5.public_id = 0) AND user_accoun
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkTimeout($timeout, $startTime) {
|
||||
if (time() - $startTime >= $timeout) {
|
||||
exit('Migration reached time limit; please run again to continue');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
@ -131,25 +181,51 @@ LEFT JOIN users u5 ON (u5.public_id IS NULL OR u5.public_id = 0) AND user_accoun
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$timeout = ini_get('max_execution_time');
|
||||
if ($timeout == 0) {
|
||||
$timeout = 600;
|
||||
}
|
||||
$timeout = max($timeout - 10, $timeout * .9);
|
||||
$startTime = time();
|
||||
|
||||
if (!Schema::hasColumn('accounts', 'pro_plan_paid')) {
|
||||
Schema::table('accounts', function($table)
|
||||
{
|
||||
$table->date('pro_plan_paid')->nullable();
|
||||
$table->date('pro_plan_trial')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
foreach (Company::all() as $company) {
|
||||
$company_ids = \DB::table('companies')
|
||||
->leftJoin('accounts', 'accounts.company_id', '=', 'companies.id')
|
||||
->whereNull('accounts.pro_plan_paid')
|
||||
->whereNull('accounts.pro_plan_trial')
|
||||
->where(function ($query) {
|
||||
$query->whereNotNull('companies.plan_paid');
|
||||
$query->orWhereNotNull('companies.trial_started');
|
||||
})
|
||||
->lists('companies.id');
|
||||
|
||||
$company_ids = array_unique($company_ids);
|
||||
|
||||
if (count($company_ids)) {
|
||||
foreach (Company::find($company_ids) as $company) {
|
||||
foreach ($company->accounts as $account) {
|
||||
$account->pro_plan_paid = $company->plan_paid;
|
||||
$account->pro_plan_trial = $company->trial_started;
|
||||
$account->save();
|
||||
}
|
||||
$this->checkTimeout($timeout, $startTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('accounts', 'company_id')) {
|
||||
Schema::table('accounts', function($table)
|
||||
{
|
||||
$table->dropForeign('accounts_company_id_foreign');
|
||||
$table->dropColumn('company_id');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::dropIfExists('companies');
|
||||
}
|
||||
|
@ -63,10 +63,11 @@ class AddPageSize extends Migration
|
||||
$table->boolean('is_early_access');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('expense_categories');
|
||||
|
||||
Schema::table('expenses', function ($table) {
|
||||
$table->dropForeign('expenses_expense_category_id_foreign');
|
||||
$table->dropColumn('expense_category_id');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('expense_categories');
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -597,24 +597,17 @@ NINJA.invoiceDetails = function(invoice) {
|
||||
])
|
||||
}
|
||||
|
||||
var isPartial = NINJA.parseFloat(invoice.partial);
|
||||
data.push([
|
||||
{text: invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']},
|
||||
{text: formatMoneyInvoice(invoice.total_amount, invoice), style: ['invoiceDetailBalanceDue']}
|
||||
])
|
||||
|
||||
if (NINJA.parseFloat(invoice.balance) < NINJA.parseFloat(invoice.amount)) {
|
||||
if (NINJA.parseFloat(invoice.partial)) {
|
||||
data.push([
|
||||
{text: invoiceLabels.balance_due},
|
||||
{text: formatMoneyInvoice(invoice.amount, invoice)}
|
||||
]);
|
||||
} else if (isPartial) {
|
||||
data.push([
|
||||
{text: invoiceLabels.balance_due},
|
||||
{text: formatMoneyInvoice(invoice.total_amount, invoice)}
|
||||
]);
|
||||
}
|
||||
|
||||
data.push([
|
||||
{text: isPartial ? invoiceLabels.partial_due : invoiceLabels.balance_due, style: ['invoiceDetailBalanceDueLabel']},
|
||||
{text: invoiceLabels.partial_due, style: ['invoiceDetailBalanceDueLabel']},
|
||||
{text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['invoiceDetailBalanceDue']}
|
||||
])
|
||||
}
|
||||
|
||||
return NINJA.prepareDataPairs(data, 'invoiceDetails');
|
||||
}
|
||||
|
@ -710,7 +710,7 @@ function calculateAmounts(invoice) {
|
||||
total += roundToTwo(invoice.custom_value2);
|
||||
}
|
||||
|
||||
invoice.total_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
|
||||
invoice.total_amount = roundToTwo(roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance)));
|
||||
invoice.discount_amount = discount;
|
||||
invoice.tax_amount1 = taxAmount1;
|
||||
invoice.tax_amount2 = taxAmount2;
|
||||
|
@ -220,6 +220,11 @@
|
||||
}
|
||||
$('#plan_term, #plan').change(updatePlanModal);
|
||||
updatePlanModal();
|
||||
|
||||
if(window.location.hash) {
|
||||
var hash = window.location.hash;
|
||||
$(hash).modal('toggle');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@stop
|
@ -190,14 +190,12 @@
|
||||
fbq('track', 'AddPaymentInfo');
|
||||
trackEvent('/account', '/submit_pro_plan/' + NINJA.proPlanFeature);
|
||||
if (NINJA.isRegistered) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '{{ URL::to('account/go_pro') }}',
|
||||
success: function(result) {
|
||||
NINJA.formIsChanged = false;
|
||||
window.location = '/payment/' + result;
|
||||
if (window.showChangePlan) {
|
||||
$('#proPlanModal').modal('hide');
|
||||
showChangePlan();
|
||||
} else {
|
||||
window.location = '/settings/account_management#changePlanModel';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('#proPlanModal').modal('hide');
|
||||
$('#go_pro').val('true');
|
||||
|
@ -20,11 +20,15 @@
|
||||
address_zip: $('#postal_code').val(),
|
||||
address_country: $("#country_id option:selected").text(),
|
||||
number: $('#card_number').val(),
|
||||
cvc: $('#cvv').val(),
|
||||
exp_month: $('#expiration_month').val(),
|
||||
exp_year: $('#expiration_year').val()
|
||||
};
|
||||
|
||||
// allow space until there's a setting to disable
|
||||
if ($('#cvv').val() != ' ') {
|
||||
data.cvc = $('#cvv').val();
|
||||
}
|
||||
|
||||
// Validate the card details
|
||||
if (!Stripe.card.validateCardNumber(data.number)) {
|
||||
$('#js-error-message').html('{{ trans('texts.invalid_card_number') }}').fadeIn();
|
||||
@ -34,7 +38,8 @@
|
||||
$('#js-error-message').html('{{ trans('texts.invalid_expiry') }}').fadeIn();
|
||||
return false;
|
||||
}
|
||||
if (!Stripe.card.validateCVC(data.cvc)) {
|
||||
|
||||
if (data.hasOwnProperty('cvc') && !Stripe.card.validateCVC(data.cvc)) {
|
||||
$('#js-error-message').html('{{ trans('texts.invalid_cvv') }}').fadeIn();
|
||||
return false;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user