Skip to main content

Upgrade Qore to version 1.0

With Qore 1.0 there have been made some changes that will break current Qore projects. This guide will attempt to make the transition as smooth as possible, by separating mandatory and optional manual changes.

info

If you prefer to look at a merge request to see what changes you need to make, you can use the following diff: Upgrade to v1.0 in Demo Project

Prerequisites

  • Make sure you have followed all upgrade guides up to version 0.9.88 that have been mentioned in the release notes.
  • Make sure your project supports using PHP >= 8.1

Getting started

  • Make the following changes to composer.json:
"require": {
+ "php": "^8.1",
- "fideloper/proxy": "^4.4",
- "laravel/framework": "^8.12",
+ "laravel/framework": "^9.0",
- "qore/system": "^0",
+ "qore/system": "^1",
},
"require-dev": {
- "facade/ignition": "^2.5",
+ "spatie/laravel-ignition": "^1.0",
- "nunomaduro/collision": "^5.0",
+ "nunomaduro/collision": "^6.1",
  • Upgrade front-end: cd frontend/src/qore && git pull origin develop
  • Run composer update
  • Publish the latest Qore migrations: php artisan vendor:publish --tag=qore.system.db
  • Run latest migrations: php artisan migrate && php artisan tenants:migrate

If you go to http://localhost:8000/ now, you should probably see the (expected) error:

Class "Fideloper\Proxy\TrustProxies" not found

Mandatory changes

Laravel 9 upgrade

Change your TrustProxies.php:

- use Fideloper\Proxy\TrustProxies as Middleware;
+ use Illuminate\Http\Middleware\TrustProxies as Middleware;

- protected $headers = Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST |
- Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB;
+ protected $headers =
+ Request::HEADER_X_FORWARDED_FOR |
+ Request::HEADER_X_FORWARDED_HOST |
+ Request::HEADER_X_FORWARDED_PORT |
+ Request::HEADER_X_FORWARDED_PROTO |
+ Request::HEADER_X_FORWARDED_AWS_ELB;

Renaming local resource names

With 1.0, we try to be more consistent with naming Resource classes. Make sure to rename your local resources (app/Resources) to append Resource at the end of the class name. For example:

- App\Resources\User
+ App\Resources\UserResource
info

The best way is to right-click on your classes in PHPStorm and refactor the name with enabling all references

Refactoring local classes

Because Qore-backend has renamed its Resource classes as well, make sure to refactor the following classes.

For example in GlobalsController.php:

- $group->addResourceMenuItem(resource(Tenant::class));
+ $group->addResourceMenuItem(resource(TenantResource::class));

- $group->addResourceMenuItem(resource(MailTemplate::class));
- $group->addResourceMenuItem(resource(MailMessage::class));
- $group->addResourceMenuItem(resource(MailFooter::class));
- $group->addResourceMenuItem(resource(TwigTemplate::class));
+ $group->addResourceMenuItem(resource(MailTemplateResource::class));
+ $group->addResourceMenuItem(resource(MailMessageResource::class));
+ $group->addResourceMenuItem(resource(MailFooterResource::class));
+ $group->addResourceMenuItem(resource(TwigTemplateResource::class));

There are multiple instances where there are references to Qore-backend resources. Make sure to check at least the following files to find wrong Qore imports:

  • app/Http/Controllers/Components/RoleAndPermissionController.php (See changes)
  • app/Http/Controllers/Globals/GlobalsController.php (See changes)
  • app/Resources/TenantResource.php (See changes)
  • app/Resources/UserResource.php (See changes)

Renaming HTTP/JSON resources

Renaming

Every JSON resource (app/Http/Resources) should include JsonResource in the name.

For example the RoleAndPermissionController:

- 'roles' => RoleResource::collection(Role::with('permissions')->get()),
- 'permissions' => PermissionResource::collection(Permission::all())
+ 'roles' => RoleJsonResource::collection(Role::with('permissions')->get()),
+ 'permissions' => PermissionJsonResource::collection(Permission::all())
info

Here again, the best way is to right-click on your classes in PHPStorm and refactor the name

AuthUserResource, TenantResource and PermissionResource

The AuthUserResource, TenantResource, and PermissionResource now exists in Qore-backend (renamed with Json in the middle). This means you will need to make the following changes.

Delete app/Http/Resources/AuthUserResource.php, app/Http/Resources/TenantResource.php and PermissionResource.php.

Change AuthController.php:

public function me(Request $request): JsonResponse
{
$userResourceClass = config('auth.providers.users.auth_resource');
return response()->json(new $userResourceClass($request->user()));
}

RoleAndPermissionController.php:

- return response()->json(['user' => new AuthUserResource($user->fresh())]);
+ $userResourceClass = config('auth.providers.users.resource');
+ return response()->json(['user' => new $userResourceClass($user->fresh())]);

RedirectIfAuthenticated.php:

- return response()->json(['guest' => true, 'user' => new AuthUserResource(auth()->user())]);
+ $userResourceClass = config('auth.providers.users.auth_resource');
+ return response()->json(['guest' => true, 'user' => new $userResourceClass(auth()->user())]);

GlobalsController.php:

+ use App\Resources\TenantResource;

return response()->json([
'menu' => $this->menu(),
- 'tenants' => TenantResource::collection(auth()->user()->tenants),
+ 'tenants' => TenantJsonResource::collection(auth()->user()->tenants),

config/auth.php:

- 'resource' => \App\Http\Resources\UserResource::class,
- 'auth_resource' => \App\Http\Resources\AuthUserResource::class
+ 'resource' => \App\Http\JsonResources\UserJsonResource::class,
+ 'auth_resource' => \Qore\System\Http\JsonResources\AuthUserJsonResource::class

Make the following change to GlobalsController.php:

- return Menu::make()
+ return (new Menu())

Authentication (Login) IP blocking

When a user logs in, the IP address should be checked instead of relying on middleware. Make sure your LoginController looks something like this:

use Qore\System\Auth\CheckIfIpAddressIsRestricted;

class LoginController extends Controller
{
use CheckIfIpAddressIsRestricted;

/**
* @param Request $request
* @return JsonResponse
* @throws TenantCouldNotBeIdentifiedById
*/
public function __invoke(Request $request): JsonResponse
{
$credentials = $request->only('email', 'password');

$user = User::where('email', $credentials['email'])->first();

if ($user && !empty($credentials['password'])) {
$credentialsMatch = Hash::check($credentials['password'], $user->password);

if ($credentialsMatch) {
/**
* Validate if user is blocked
*/
if ($user->blocked) {
return response()->json([
'message' => __('This account has been blocked'),
'blocked' => true,
'block_reason' => $user->block_reason
], 422);
}

tenancy()->initialize($user->tenant_id ?? $user->tenants()->first());

/**
* Validate IP address
*/
if (!$this->validateIpAddress($request)) {
activity()->performedOn($user)
->withProperties(['note' => __('Location') . ': ' . $request->ip()])
->log(__('Attempt from invalid IP address'));

return response()->json([
'message' => __('You are not allowed to use the application from this location'),
'code' => 'ipblock'
], 422);
}
}

if (Auth::attempt($credentials)) {
$request->session()->regenerate();

$userResourceClass = config('auth.providers.users.auth_resource');
return response()->json(['user' => new $userResourceClass(auth()->user())]);
}
}

return response()->json(['errors' => ['email' => [__('auth.failed')]]], 422);
}
}

Next, upgrade your auth/Login.vue:

- <!-- USER BLOCKED ERROR -->
+ <!-- USER AUTHENTICATION ERROR -->
<base-message
- v-if="errors.blocked"
+ v-if="errors.message"
class="bg-red-8 text-white"
icon="message"

When an error occurs, sometimes the login button would keep hanging. You can fix this by adding the following in the same file:

  mounted () {
if (this.status === 'authenticating') {
this.$store.dispatch('auth/reset')
}
},

Spatie Activity log

Spatie Activity log packages made changes from laravel 8 to 9. Every model that uses the use LogsActivity trait, should change the following:

-    /**
- * @var string[]
- */
- protected static $logAttributes = ['*'];
-
- /**
- * @var bool
- */
- protected static $logOnlyDirty = true;
-
- /**
- * @var bool
- */
- protected static $submitEmptyLogs = false;

+ public function getActivitylogOptions(): LogOptions
+ {
+ return LogOptions::defaults()->logAll()->logOnlyDirty()->dontSubmitEmptyLogs();
+ }
info

There is a high chance you don't have models that include this trait, so you can ignore this.

Add missing Policies

Some policies have been removed from Qore-backend and should be created & registered manually now.

Add the policy TwigTemplatePolicy.php:

<?php

namespace App\Policies;

use Illuminate\Auth\Access\HandlesAuthorization;
use Qore\System\Models\Central\User;
use Qore\System\Models\Tenant\TwigTemplate;

class TwigTemplatePolicy
{
use HandlesAuthorization;

/**
* @param User $user
* @return bool
*/
public function viewAny(User $user)
{
return true;
}

/**
* @param User $user
* @return bool
*/
public function view(User $user)
{
return true;
}

/**
* @param User $user
* @return bool
*/
public function create(User $user)
{
return true;
}

/**
* @param User $user
* @param TwigTemplate $template
* @return bool
*/
public function update(User $user, TwigTemplate $template)
{
return $template->user_id === $user->id;
}

/**
* @param User $user
* @param TwigTemplate $template
* @return bool
*/
public function delete(User $user, TwigTemplate $template)
{
return $template->user_id === $user->id;
}
}

AuthServiceProvider.php:

+ use App\Policies\TwigTemplatePolicy;
+ use Qore\System\Models\Tenant\TwigTemplate;

+ TwigTemplate::class => TwigTemplatePolicy::class

Fix TenantVariablePolicy

Update the TenantVariablePolicy.php:

- use Qore\System\Resources\TenantVariable;
+ use Qore\System\Models\Tenant\TenantVariable;

2FA bugfix

When logging in while 2FA is required, it will show a Secret key but the value is undefined. Make the following change.

TwoFactorAuth/Field.vue:

- <div class="full-width secret-field">
+ <div
+ v-if="secret"
+ class="full-width secret-field"
+ >

Currency changes

Notable changes:

  • Changed $formatPrice Helper to make use of the new Currency Database View
  • Introduce new Currency Database View that will make use of Countries Data to make easier access for currencies
  • Change the currencies() helper method to use the new Currency Database View
  • Removed CurrenciesNotFoundException

Remove the ExportCountriesSymbols.php file from your project as it's not going to be used anymore.

info

Make sure to remove references to CurrenciesNotFoundException as this Exception has been removed.

caution

While the structure that the helper currencies() function on the backend didn't change the .json file with currencies on the frontend will now not be present anymore.

It will now be found under $activeTenant.currencies with the following structure:

$activeTenant.currencies = {
"EUR": {
"country_id": 56,
"currency": "euro",
"currency_code": "EUR",
"currency_symbol": "€"
},
...
}

Modules & Plugins upgrades

There are multiple modules and plugins that require some manual changes.

Make sure to upgrade to the latest version for each of the upgraded extensions in composer.json:

+    "qore/address": "^1",
+ "qore/attribute": "^2",
+ "qore/crm": "^0",
+ "qore/excel-import": "^1"
+ "qore/invoicing": "^2",
+ "qore/layout": "^1",
+ "qore/mailing-extended": "^1"
+ "qore/notes": "^1",

If your plugin/module is not in this list, no changes have been made.

Address module

tip

No manual changes required after updating to ^1

Attribute module

Make sure to upgrade to ^2

Because the config/attribute.php was not serializable, you will have to re-publish the config file (make sure u temporary back-up your old config file):

php artisan vendor:publish --tag=qore.attribute.config --force

The attribute_value_resolver and types in the config file require callable classes now instead of Closures.

And then make sure you re-add every resource in the resources config array, and any other configs u may have added.

See example here.

CRM module

tip

No manual changes required after updating to ^0

Excel Import module

tip

No manual changes required after updating to ^1

Invoicing module

Make sure to upgrade to ^2

Because the config/invoicing.php was not serializable, you will have to re-publish the config file (make sure u temporary back-up your old config file):

php artisan vendor:publish --tag=qore.invoicing.config --force

Your current config file will look quite different, because every closure is replaced with callable classes. This means the following config keys are changed:

  • invoice_line_fields
  • invoice_lines
  • template_variables
  • email
  • receiver

See example here.

Layout module

tip

No manual changes required after updating to ^1

Mailing extended module

tip

No manual changes required after updating to ^1

Notes module

tip

No manual changes required after updating to ^1

Optional changes

Typos

In the MainHeader.vue add the following changes:

- <profile-downdown :user="user" />
+ <profile-dropdown :user="user" />

- <profile-downdown
+ <profile-dropdown
+ :user="user"
+ class="fit"
+ />

- import ProfileDowndown from './ProfileDowndown.vue'
+ import ProfileDropdown from './ProfileDropdown.vue'

components: {
CompanySwitcher,
- ProfileDowndown,
+ ProfileDropdown,
DownloadMenu
},

And rename ProfileDowndown.vue to ProfileDropdown.vue.

In axios.js add the following change:

- message: router.app.$t('You are not allowed to use the appliction from this location')
+ message: router.app.$t('You are not allowed to use the application from this location')

Add translations

frontend/src/locales/nl.json:

"Show logs": "Toon logboeken",
"Impersonating {user}": "Voor doen als {user}",
"Leave impersonate": "Terugkeren",
"Impersonated by {user}": "Voorgedaan door {user}",
"You are not allowed to use the application from this location": "U bent niet gemachtigd om de applicatie van deze locatie te gebruiken",
"Welcome": "Welkom",
"Logs": "Logs",
"No details found": "Geen details gevonden"

resources/lang/nl.json:

"Impersonate user": "Voor doen als gebruiker",
"Impersonating :user": "Voor doen als :user",
"Access to mail templates": "Toegang tot mail sjablonen",
"Impersonate users": "Voor doen als gebruikers",
"You are not allowed to use the application from this location": "U bent niet gemachtigd om de applicatie van deze locatie te gebruiken",
"Attempt from invalid IP address": "Poging vanaf ongeldig IP adres"

Impersonation

To allow for impersonation, create the following action:

<?php

namespace App\Actions\User;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Session;
use Qore\System\Resource\Action\Action;

class ImpersonateUser extends Action
{
public function __construct()
{
$this->hideOnIndex();

$this->canSee(function () {
return auth()->user()->hasPermissionTo('impersonate users');
});

$this->canRun(function ($model) {
return auth()->user()->hasPermissionTo('impersonate users') &&
$model->id !== auth()->user()->id;
});
}

/**
* @return string
*/
public function icon(): string
{
return 'admin_panel_settings';
}

/**
* @return string
*/
public function title(): string
{
return __('Impersonate user');
}

/**
* @param Model $model
* @param array $arguments
* @return void
*/
public function run(Model $model, array $arguments): void
{
Session::put('impersonator', auth()->user()->id);

auth('web')->logout();
auth('web')->loginUsingId($model->id);

Session::put('impersonate', $model->id);
}

/**
* @param Collection $models
* @param array $arguments
* @return string|null
*/
public function redirectTo(Collection $models, array $arguments): ?string
{
return '/';
}
}

Create the following PermissionScope:

<?php

namespace App\Http\PermissionScopes;

use Illuminate\Database\Eloquent\Model;
use Qore\System\Http\Permission\PermissionScope;
use Qore\System\Models\Central\User;

class ImpersonateUsers extends PermissionScope
{
/**
* @param User $user
* @param Model|null $model
* @return bool
*/
public function check(User $user, ?Model $model): bool
{
return $user->id !== $model->id;
}

/**
* @return string
*/
public function description(): string
{
return __('Impersonate users');
}
}

Add the ImpersonateUsers action to UserResource:

return new ActionCollection(
(new ImpersonateUser())
// ..

Add to PermissionsSeeder:

    $creator->create('impersonate users',
'users',
true,
true,
true,
true,
true,
ImpersonateUsers::class
);

Add a banner to the front-end when the user is impersonating.

MainHeader.vue on line 6:

    <q-banner
v-if="$store.state.auth.isImpersonating"
dense
inline-actions
class="text-white bg-red"
>
{{ $t('Impersonating {user}', {user: $me.name}) }}
<q-btn
:label="$t('Leave impersonate')"
size="sm"
color="white"
no-caps
outline
class="q-ml-md"
@click="leaveImpersonation"
/>
</q-banner>

Also add a function to leave impersonation in MainHeader.vue:

leaveImpersonation () {
this.$store.dispatch('auth/leaveImpersonation')

this.$q.notify({
classes: 'action-executed-notification',
message: this.$t('Action successfully executed!'),
type: 'positive',
position: 'top',
actions: [{ icon: 'close', color: 'white' }]
})
}

And (re)run your seeders.