diff --git a/config.demo/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php b/config.demo/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php deleted file mode 100644 index 4896bd01..00000000 --- a/config.demo/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php +++ /dev/null @@ -1,42 +0,0 @@ -first(); - - if ($beta_sku) { - $beta_sku->name = 'Private Beta (invitation only)'; - $beta_sku->description = 'Access to the private beta program subscriptions'; - $beta_sku->save(); - } - - $meet_sku = \App\Sku::where('title', 'meet')->first(); - - if ($meet_sku) { - $meet_sku->name = 'Voice & Video Conferencing (public beta)'; - $meet_sku->handler_class = 'App\Handlers\Meet'; - $meet_sku->save(); - } - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - } -} diff --git a/config.demo/src/database/migrations/2021_02_19_100000_transaction_amount_fix.php b/config.demo/src/database/migrations/2021_02_19_100000_transaction_amount_fix.php deleted file mode 100644 index 19277aa2..00000000 --- a/config.demo/src/database/migrations/2021_02_19_100000_transaction_amount_fix.php +++ /dev/null @@ -1,38 +0,0 @@ -get()->each(function ($sku) { - $sku->title = 'beta-distlists'; - $sku->handler_class = 'App\Handlers\Beta\Distlists'; - $sku->save(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - \App\Sku::where('title', 'beta-distlists')->get()->each(function ($sku) { - $sku->title = 'distlist'; - $sku->handler_class = 'App\Handlers\Distlist'; - $sku->save(); - }); - } -} diff --git a/config.demo/src/database/migrations/2022_05_13_090000_permissions_and_room_subscriptions.php b/config.demo/src/database/migrations/2022_05_13_090000_permissions_and_room_subscriptions.php deleted file mode 100644 index 0cabaf4f..00000000 --- a/config.demo/src/database/migrations/2022_05_13_090000_permissions_and_room_subscriptions.php +++ /dev/null @@ -1,57 +0,0 @@ - 'group-room', - 'name' => 'Group conference room', - 'description' => 'Shareable audio & video conference room', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\GroupRoom', - 'active' => true, - ]); - - \App\Sku::create([ - 'title' => 'room', - 'name' => 'Standard conference room', - 'description' => 'Audio & video conference room', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Room', - 'active' => true, - ]); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - \App\Sku::where('title', 'room')->delete(); - \App\Sku::where('title', 'group-room')->delete(); - - \App\Sku::create([ - 'title' => 'meet', - 'name' => 'Voice & Video Conferencing (public beta)', - 'description' => 'Video conferencing tool', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Meet', - 'active' => true, - ]); - } -}; diff --git a/config.demo/src/database/migrations/2022_07_08_100000_fix_group_sku_name.php b/config.demo/src/database/migrations/2022_07_08_100000_fix_group_sku_name.php deleted file mode 100644 index b044f93e..00000000 --- a/config.demo/src/database/migrations/2022_07_08_100000_fix_group_sku_name.php +++ /dev/null @@ -1,32 +0,0 @@ -get()->each(function ($sku) { - $sku->name = 'Distribution list'; - $sku->description = 'Mail distribution list'; - $sku->save(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - \App\Sku::where('title', 'group')->get()->each(function ($sku) { - $sku->name = 'Group'; - $sku->description = 'Distribution list'; - $sku->save(); - }); - } -}; diff --git a/config.demo/src/database/migrations/2022_09_08_100001_plans_free_months.php b/config.demo/src/database/migrations/2022_09_08_100001_plans_free_months.php deleted file mode 100644 index 4453990d..00000000 --- a/config.demo/src/database/migrations/2022_09_08_100001_plans_free_months.php +++ /dev/null @@ -1,26 +0,0 @@ -update(['free_months' => 1]); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - } -}; diff --git a/config.prod/src/database/migrations b/config.prod/src/database/migrations deleted file mode 120000 index dae12a41..00000000 --- a/config.prod/src/database/migrations +++ /dev/null @@ -1 +0,0 @@ -../../../config.demo/src/database/migrations \ No newline at end of file diff --git a/config.prod/src/database/seeds/AdminSeeder.php b/config.prod/src/database/seeds/AdminSeeder.php index 35e0758b..9c291119 100644 --- a/config.prod/src/database/seeds/AdminSeeder.php +++ b/config.prod/src/database/seeds/AdminSeeder.php @@ -1,86 +1,218 @@ 'mailbox', + 'name' => 'User Mailbox', + 'description' => 'Just a mailbox', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Mailbox', + 'active' => true, + ], + [ + 'title' => 'domain', + 'name' => 'Hosted Domain', + 'description' => 'Somewhere to place a mailbox', + 'cost' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Domain', + 'active' => false, + ], + [ + 'title' => 'domain-hosting', + 'name' => 'External Domain', + 'description' => 'Host a domain that is externally registered', + 'cost' => 0, + 'units_free' => 1, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\DomainHosting', + 'active' => true, + ], + [ + 'title' => 'storage', + 'name' => 'Storage Quota', + 'description' => 'Some wiggle room', + 'cost' => 0, + 'units_free' => 5, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Storage', + 'active' => true, + ], + [ + 'title' => 'groupware', + 'name' => 'Groupware Features', + 'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Groupware', + 'active' => true, + ], + [ + 'title' => 'resource', + 'name' => 'Resource', + 'description' => 'Reservation taker', + 'cost' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Resource', + 'active' => true, + ], + [ + 'title' => 'shared-folder', + 'name' => 'Shared Folder', + 'description' => 'A shared folder', + 'cost' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\SharedFolder', + 'active' => true, + ], + [ + 'title' => '2fa', + 'name' => '2-Factor Authentication', + 'description' => 'Two factor authentication for webmail and administration panel', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Auth2F', + 'active' => true, + ], + [ + 'title' => 'activesync', + 'name' => 'Activesync', + 'description' => 'Mobile synchronization', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Activesync', + 'active' => true, + ], + [ + 'title' => 'group', + 'name' => 'Distribution list', + 'description' => 'Mail distribution list', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Group', + 'active' => true, + ], + [ + 'title' => 'group-room', + 'name' => 'Group conference room', + 'description' => 'Shareable audio & video conference room', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\GroupRoom', + 'active' => true, + ], + [ + 'title' => 'room', + 'name' => 'Standard conference room', + 'description' => 'Audio & video conference room', + 'cost' => 0, + 'units_free' => 0, + 'period' => 'monthly', + 'handler_class' => 'App\Handlers\Room', + 'active' => true, + ], + ]; + + foreach ($skus as $sku) { + // Check existence because migration might have added this already + if (!Sku::where('title', $sku['title'])->where('tenant_id', \config('app.tenant_id'))->first()) { + Sku::create($sku); + } + } + $skuDomain = Sku::where(['title' => 'domain-hosting', 'tenant_id' => \config('app.tenant_id')])->first(); $skuGroupware = Sku::where(['title' => 'groupware', 'tenant_id' => \config('app.tenant_id')])->first(); $skuMailbox = Sku::where(['title' => 'mailbox', 'tenant_id' => \config('app.tenant_id')])->first(); $skuStorage = Sku::where(['title' => 'storage', 'tenant_id' => \config('app.tenant_id')])->first(); + // $skuGroup = Sku::where(['title' => 'group', 'tenant_id' => \config('app.tenant_id')])->first(); - $packageKolab = Package::create( + $userPackage = Package::create( [ 'title' => 'kolab', 'name' => 'Groupware Account', 'description' => 'A fully functional groupware account.', 'discount_rate' => 0, ] ); - $packageKolab->skus()->saveMany([ + + $userPackage->skus()->saveMany([ $skuMailbox, $skuGroupware, $skuStorage ]); - - $packageDomain = Package::create( - [ - 'title' => 'domain-hosting', - 'name' => 'Domain Hosting', - 'description' => 'Use your own, existing domain.', - 'discount_rate' => 0, - ] + // This package contains 2 units of the storage SKU, which just so happens to also + // be the number of SKU free units. + $userPackage->skus()->updateExistingPivot( + $skuStorage, + ['qty' => 5], + false ); - $packageDomain->skus()->saveMany([ - $skuDomain - ]); - - //Create primary domain - $appDomain = Domain::create( - [ - 'namespace' => \config('app.domain'), - 'status' => DOMAIN::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC, - ] - ); //Create admin user $admin = User::create( [ 'email' => 'admin@' . \config('app.domain'), 'password' => \App\Utils::generatePassphrase() ] ); $admin->setSettings( [ 'first_name' => 'Admin', ] ); - $appDomain->assignPackage($packageDomain, $admin); - $admin->assignPackage($packageKolab); + $admin->assignPackage($userPackage); + + + //Create primary domain + $domain = Domain::create( + [ + 'namespace' => \config('app.domain'), + 'status' => DOMAIN::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, + 'type' => Domain::TYPE_EXTERNAL, + ] + ); + + $domainPackage = Package::create( + [ + 'title' => 'domain', + 'name' => 'Domain', + 'description' => 'Domain.', + 'discount_rate' => 0, + ] + ); + $domainPackage->skus()->saveMany([$skuDomain]); + + $domain->assignPackage($domainPackage, $admin); } } - diff --git a/config.prod/src/database/seeds/DatabaseSeeder.php b/config.prod/src/database/seeds/DatabaseSeeder.php index 9388bf56..fe7dbe0b 100644 --- a/config.prod/src/database/seeds/DatabaseSeeder.php +++ b/config.prod/src/database/seeds/DatabaseSeeder.php @@ -1,23 +1,23 @@ call([ Seeds\PassportSeeder::class, Seeds\PowerDNSSeeder::class, - Seeds\SkuSeeder::class, Seeds\AdminSeeder::class, + Seeds\ImapAdminSeeder::class, ]); } } diff --git a/config.prod/src/database/seeds/ImapAdminSeeder.php b/config.prod/src/database/seeds/ImapAdminSeeder.php new file mode 100644 index 00000000..4f18d203 --- /dev/null +++ b/config.prod/src/database/seeds/ImapAdminSeeder.php @@ -0,0 +1,26 @@ + \config('imap.admin_login'), + 'password' => \config('imap.admin_password') + ] + ); + } +} diff --git a/config.prod/src/database/seeds/SkuSeeder.php b/config.prod/src/database/seeds/SkuSeeder.php deleted file mode 100644 index cceae31d..00000000 --- a/config.prod/src/database/seeds/SkuSeeder.php +++ /dev/null @@ -1,172 +0,0 @@ - 'mailbox', - 'name' => 'User Mailbox', - 'description' => 'Just a mailbox', - 'cost' => 500, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Mailbox', - 'active' => true, - ], - [ - 'title' => 'domain', - 'name' => 'Hosted Domain', - 'description' => 'Somewhere to place a mailbox', - 'cost' => 100, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Domain', - 'active' => false, - ], - [ - 'title' => 'domain-registration', - 'name' => 'Domain Registration', - 'description' => 'Register a domain with us', - 'cost' => 101, - 'period' => 'yearly', - 'handler_class' => 'App\Handlers\DomainRegistration', - 'active' => false, - ], - [ - 'title' => 'domain-hosting', - 'name' => 'External Domain', - 'description' => 'Host a domain that is externally registered', - 'cost' => 100, - 'units_free' => 1, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\DomainHosting', - 'active' => true, - ], - [ - 'title' => 'domain-relay', - 'name' => 'Domain Relay', - 'description' => 'A domain you host at home, for which we relay email', - 'cost' => 103, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\DomainRelay', - 'active' => false, - ], - [ - 'title' => 'storage', - 'name' => 'Storage Quota', - 'description' => 'Some wiggle room', - 'cost' => 25, - 'units_free' => 5, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Storage', - 'active' => true, - ], - [ - 'title' => 'groupware', - 'name' => 'Groupware Features', - 'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.', - 'cost' => 490, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Groupware', - 'active' => true, - ], - [ - 'title' => 'resource', - 'name' => 'Resource', - 'description' => 'Reservation taker', - 'cost' => 101, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Resource', - 'active' => true, - ], - [ - 'title' => 'shared-folder', - 'name' => 'Shared Folder', - 'description' => 'A shared folder', - 'cost' => 89, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\SharedFolder', - 'active' => true, - ], - [ - 'title' => '2fa', - 'name' => '2-Factor Authentication', - 'description' => 'Two factor authentication for webmail and administration panel', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Auth2F', - 'active' => true, - ], - [ - 'title' => 'activesync', - 'name' => 'Activesync', - 'description' => 'Mobile synchronization', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Activesync', - 'active' => true, - ], - [ - 'title' => 'beta', - 'name' => 'Private Beta (invitation only)', - 'description' => 'Access to the private beta program features', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta', - 'active' => false, - ], - [ - 'title' => 'group', - 'name' => 'Distribution list', - 'description' => 'Mail distribution list', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Group', - 'active' => true, - ], - [ - 'title' => 'group-room', - 'name' => 'Group conference room', - 'description' => 'Shareable audio & video conference room', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\GroupRoom', - 'active' => true, - ], - [ - 'title' => 'room', - 'name' => 'Standard conference room', - 'description' => 'Audio & video conference room', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Room', - 'active' => true, - ], - ]; - - foreach ($skus as $sku) { - // Check existence because migration might have added this already - if (!Sku::where('title', $sku['title'])->where('tenant_id', \config('app.tenant_id'))->first()) { - Sku::create($sku); - } - } - } -} diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php index 71fbc606..559bb167 100644 --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -1,710 +1,711 @@ guard()->user(); $search = trim(request()->input('search')); $page = intval(request()->input('page')) ?: 1; $pageSize = 20; $hasMore = false; $result = $user->users(); // Search by user email, alias or name if (strlen($search) > 0) { // thanks to cloning we skip some extra queries in $user->users() $allUsers1 = clone $result; $allUsers2 = clone $result; $result->whereLike('email', $search) ->union( $allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id') ->whereLike('alias', $search) ) ->union( $allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id') ->whereLike('value', $search) ->whereIn('key', ['first_name', 'last_name']) ); } $result = $result->orderBy('email') ->limit($pageSize + 1) ->offset($pageSize * ($page - 1)) ->get(); if (count($result) > $pageSize) { $result->pop(); $hasMore = true; } // Process the result $result = $result->map( function ($user) { return $this->objectToClient($user); } ); $result = [ 'list' => $result, 'count' => count($result), 'hasMore' => $hasMore, ]; return response()->json($result); } /** * Display information on the user account specified by $id. * * @param string $id The account to show information for. * * @return \Illuminate\Http\JsonResponse */ public function show($id) { $user = User::find($id); if (!$this->checkTenant($user)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canRead($user)) { return $this->errorResponse(403); } $response = $this->userResponse($user); $response['skus'] = \App\Entitlement::objectEntitlementsSummary($user); $response['config'] = $user->getConfig(); $response['aliases'] = $user->aliases()->pluck('alias')->all(); $code = $user->verificationcodes()->where('active', true) ->where('expires_at', '>', \Carbon\Carbon::now()) ->first(); if ($code) { $response['passwordLinkCode'] = $code->short_code . '-' . $code->code; } return response()->json($response); } /** * User status (extended) information * * @param \App\User $user User object * * @return array Status information */ public static function statusInfo($user): array { $process = self::processStateInfo( $user, [ 'user-new' => true, 'user-ldap-ready' => $user->isLdapReady(), 'user-imap-ready' => $user->isImapReady(), ] ); // Check if the user is a controller of his wallet $isController = $user->canDelete($user); $isDegraded = $user->isDegraded(); $hasMeet = !$isDegraded && Sku::withObjectTenantContext($user)->where('title', 'room')->exists(); + // Enable all features if there are no skus for domain-hosting $hasCustomDomain = $user->wallet()->entitlements() ->where('entitleable_type', Domain::class) - ->count() > 0; + ->count() > 0 || !Sku::withObjectTenantContext($user)->where('title', 'domain-hosting')->exists(); // Get user's entitlements titles $skus = $user->entitlements()->select('skus.title') ->join('skus', 'skus.id', '=', 'entitlements.sku_id') ->get() ->pluck('title') ->sort() ->unique() ->values() ->all(); - $hasBeta = in_array('beta', $skus); + $hasBeta = in_array('beta', $skus) || !Sku::withObjectTenantContext($user)->where('title', 'beta')->exists(); $plan = $isController ? $user->wallet()->plan() : null; $result = [ 'skus' => $skus, - 'enableBeta' => in_array('beta', $skus), + 'enableBeta' => $hasBeta, // TODO: This will change when we enable all users to create domains 'enableDomains' => $isController && $hasCustomDomain, // TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners 'enableDistlists' => $isController && $hasCustomDomain && $hasBeta, 'enableFiles' => !$isDegraded && $hasBeta && \config('app.with_files'), // TODO: Make 'enableFolders' working for wallet controllers that aren't account owners 'enableFolders' => $isController && $hasCustomDomain && $hasBeta, // TODO: Make 'enableResources' working for wallet controllers that aren't account owners 'enableResources' => $isController && $hasCustomDomain && $hasBeta, 'enableRooms' => $hasMeet, 'enableSettings' => $isController, 'enableUsers' => $isController, 'enableWallets' => $isController, 'enableWalletMandates' => $isController, 'enableWalletPayments' => $isController && (!$plan || $plan->mode != Plan::MODE_MANDATE), 'enableCompanionapps' => $hasBeta, ]; return array_merge($process, $result); } /** * Create a new user record. * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\JsonResponse The response */ public function store(Request $request) { $current_user = $this->guard()->user(); $owner = $current_user->walletOwner(); if ($owner->id != $current_user->id) { return $this->errorResponse(403); } $this->deleteBeforeCreate = null; if ($error_response = $this->validateUserRequest($request, null, $settings)) { return $error_response; } if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) { $errors = ['package' => \trans('validation.packagerequired')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } if ($package->isDomain()) { $errors = ['package' => \trans('validation.packageinvalid')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } DB::beginTransaction(); // @phpstan-ignore-next-line if ($this->deleteBeforeCreate) { $this->deleteBeforeCreate->forceDelete(); } // Create user record $user = User::create([ 'email' => $request->email, 'password' => $request->password, 'status' => $owner->isRestricted() ? User::STATUS_RESTRICTED : 0, ]); $this->activatePassCode($user); $owner->assignPackage($package, $user); if (!empty($settings)) { $user->setSettings($settings); } if (!empty($request->aliases)) { $user->setAliases($request->aliases); } DB::commit(); return response()->json([ 'status' => 'success', 'message' => \trans('app.user-create-success'), ]); } /** * Update user data. * * @param \Illuminate\Http\Request $request The API request. * @param string $id User identifier * * @return \Illuminate\Http\JsonResponse The response */ public function update(Request $request, $id) { $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); } $current_user = $this->guard()->user(); // TODO: Decide what attributes a user can change on his own profile if (!$current_user->canUpdate($user)) { return $this->errorResponse(403); } if ($error_response = $this->validateUserRequest($request, $user, $settings)) { return $error_response; } // Entitlements, only controller can do that if ($request->skus !== null && !$current_user->canDelete($user)) { return $this->errorResponse(422, "You have no permission to change entitlements"); } DB::beginTransaction(); SkusController::updateEntitlements($user, $request->skus); if (!empty($settings)) { $user->setSettings($settings); } if (!empty($request->password)) { $user->password = $request->password; $user->save(); } $this->activatePassCode($user); if (isset($request->aliases)) { $user->setAliases($request->aliases); } DB::commit(); $response = [ 'status' => 'success', 'message' => \trans('app.user-update-success'), ]; // For self-update refresh the statusInfo in the UI if ($user->id == $current_user->id) { $response['statusInfo'] = self::statusInfo($user); } return response()->json($response); } /** * Create a response data array for specified user. * * @param \App\User $user User object * * @return array Response data */ public static function userResponse(User $user): array { $response = array_merge($user->toArray(), self::objectState($user)); $wallet = $user->wallet(); // IsLocked flag to lock the user to the Wallet page only $response['isLocked'] = (!$user->isActive() && ($plan = $wallet->plan()) && $plan->mode == Plan::MODE_MANDATE); // Settings $response['settings'] = []; foreach ($user->settings()->whereIn('key', self::USER_SETTINGS)->get() as $item) { $response['settings'][$item->key] = $item->value; } // Status info $response['statusInfo'] = self::statusInfo($user); // Add more info to the wallet object output $map_func = function ($wallet) use ($user) { $result = $wallet->toArray(); if ($wallet->discount) { $result['discount'] = $wallet->discount->discount; $result['discount_description'] = $wallet->discount->description; } if ($wallet->user_id != $user->id) { $result['user_email'] = $wallet->owner->email; } $provider = \App\Providers\PaymentProvider::factory($wallet); $result['provider'] = $provider->name(); return $result; }; // Information about wallets and accounts for access checks $response['wallets'] = $user->wallets->map($map_func)->toArray(); $response['accounts'] = $user->accounts->map($map_func)->toArray(); $response['wallet'] = $map_func($wallet); return $response; } /** * Prepare user statuses for the UI * * @param \App\User $user User object * * @return array Statuses array */ protected static function objectState($user): array { $state = parent::objectState($user); $state['isAccountDegraded'] = $user->isDegraded(true); return $state; } /** * Validate user input * * @param \Illuminate\Http\Request $request The API request. * @param \App\User|null $user User identifier * @param array $settings User settings (from the request) * * @return \Illuminate\Http\JsonResponse|null The error response on error */ protected function validateUserRequest(Request $request, $user, &$settings = []) { $rules = [ 'external_email' => 'nullable|email', 'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', 'first_name' => 'string|nullable|max:128', 'last_name' => 'string|nullable|max:128', 'organization' => 'string|nullable|max:512', 'billing_address' => 'string|nullable|max:1024', 'country' => 'string|nullable|alpha|size:2', 'currency' => 'string|nullable|alpha|size:3', 'aliases' => 'array|nullable', ]; $controller = ($user ?: $this->guard()->user())->walletOwner(); // Handle generated password reset code if ($code = $request->input('passwordLinkCode')) { // Accept - input if (strpos($code, '-')) { $code = explode('-', $code)[1]; } $this->passCode = $this->guard()->user()->verificationcodes() ->where('code', $code)->where('active', false)->first(); // Generate a password for a new user with password reset link // FIXME: Should/can we have a user with no password set? if ($this->passCode && empty($user)) { $request->password = $request->password_confirmation = Str::random(16); $ignorePassword = true; } } if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) { if (empty($ignorePassword)) { $rules['password'] = ['required', 'confirmed', new Password($controller)]; } } $errors = []; // Validate input $v = Validator::make($request->all(), $rules); if ($v->fails()) { $errors = $v->errors()->toArray(); } // For new user validate email address if (empty($user)) { $email = $request->email; if (empty($email)) { $errors['email'] = \trans('validation.required', ['attribute' => 'email']); } elseif ($error = self::validateEmail($email, $controller, $this->deleteBeforeCreate)) { $errors['email'] = $error; } } // Validate aliases input if (isset($request->aliases)) { $aliases = []; $existing_aliases = $user ? $user->aliases()->get()->pluck('alias')->toArray() : []; foreach ($request->aliases as $idx => $alias) { if (is_string($alias) && !empty($alias)) { // Alias cannot be the same as the email address (new user) if (!empty($email) && Str::lower($alias) == Str::lower($email)) { continue; } // validate new aliases if ( !in_array($alias, $existing_aliases) && ($error = self::validateAlias($alias, $controller)) ) { if (!isset($errors['aliases'])) { $errors['aliases'] = []; } $errors['aliases'][$idx] = $error; continue; } $aliases[] = $alias; } } $request->aliases = $aliases; } if (!empty($errors)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } // Update user settings $settings = $request->only(array_keys($rules)); unset($settings['password'], $settings['aliases'], $settings['email']); return null; } /** * Execute (synchronously) specified step in a user setup process. * * @param \App\User $user User object * @param string $step Step identifier (as in self::statusInfo()) * * @return bool|null True if the execution succeeded, False if not, Null when * the job has been sent to the worker (result unknown) */ public static function execProcessStep(User $user, string $step): ?bool { try { if (strpos($step, 'domain-') === 0) { return DomainsController::execProcessStep($user->domain(), $step); } switch ($step) { case 'user-ldap-ready': case 'user-imap-ready': // Use worker to do the job, frontend might not have the IMAP admin credentials \App\Jobs\User\CreateJob::dispatch($user->id); return null; } } catch (\Exception $e) { \Log::error($e); } return false; } /** * Email address validation for use as a user mailbox (login). * * @param string $email Email address * @param \App\User $user The account owner * @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group * with the specified email address, if exists * * @return ?string Error message on validation error */ public static function validateEmail(string $email, \App\User $user, &$deleted = null): ?string { $deleted = null; if (strpos($email, '@') === false) { return \trans('validation.entryinvalid', ['attribute' => 'email']); } list($login, $domain) = explode('@', Str::lower($email)); if (strlen($login) === 0 || strlen($domain) === 0) { return \trans('validation.entryinvalid', ['attribute' => 'email']); } // Check if domain exists $domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); } // Validate login part alone $v = Validator::make( ['email' => $login], ['email' => ['required', new UserEmailLocal(!$domain->isPublic())]] ); if ($v->fails()) { return $v->errors()->toArray()['email'][0]; } // Check if it is one of domains available to the user if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) { return \trans('validation.entryexists', ['attribute' => 'domain']); } // Check if a user/group/resource/shared folder with specified address already exists if ( ($existing = User::emailExists($email, true)) || ($existing = \App\Group::emailExists($email, true)) || ($existing = \App\Resource::emailExists($email, true)) || ($existing = \App\SharedFolder::emailExists($email, true)) ) { // If this is a deleted user/group/resource/folder in the same custom domain // we'll force delete it before creating the target user if (!$domain->isPublic() && $existing->trashed()) { $deleted = $existing; } else { return \trans('validation.entryexists', ['attribute' => 'email']); } } // Check if an alias with specified address already exists. if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) { return \trans('validation.entryexists', ['attribute' => 'email']); } return null; } /** * Email address validation for use as an alias. * * @param string $email Email address * @param \App\User $user The account owner * * @return ?string Error message on validation error */ public static function validateAlias(string $email, \App\User $user): ?string { if (strpos($email, '@') === false) { return \trans('validation.entryinvalid', ['attribute' => 'alias']); } list($login, $domain) = explode('@', Str::lower($email)); if (strlen($login) === 0 || strlen($domain) === 0) { return \trans('validation.entryinvalid', ['attribute' => 'alias']); } // Check if domain exists $domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); } // Validate login part alone $v = Validator::make( ['alias' => $login], ['alias' => ['required', new UserEmailLocal(!$domain->isPublic())]] ); if ($v->fails()) { return $v->errors()->toArray()['alias'][0]; } // Check if it is one of domains available to the user if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) { return \trans('validation.entryexists', ['attribute' => 'domain']); } // Check if a user with specified address already exists if ($existing_user = User::emailExists($email, true)) { // Allow an alias in a custom domain to an address that was a user before if ($domain->isPublic() || !$existing_user->trashed()) { return \trans('validation.entryexists', ['attribute' => 'alias']); } } // Check if a group/resource/shared folder with specified address already exists if ( \App\Group::emailExists($email) || \App\Resource::emailExists($email) || \App\SharedFolder::emailExists($email) ) { return \trans('validation.entryexists', ['attribute' => 'alias']); } // Check if an alias with specified address already exists if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) { // Allow assigning the same alias to a user in the same group account, // but only for non-public domains if ($domain->isPublic()) { return \trans('validation.entryexists', ['attribute' => 'alias']); } } return null; } /** * Activate password reset code (if set), and assign it to a user. * * @param \App\User $user The user */ protected function activatePassCode(User $user): void { // Activate the password reset code if ($this->passCode) { $this->passCode->user_id = $user->id; $this->passCode->active = true; $this->passCode->save(); } } } diff --git a/src/database/migrations/2020_10_29_100000_add_beta_skus.php b/src/database/migrations/2020_10_29_100000_add_beta_skus.php deleted file mode 100644 index 3cf3ed12..00000000 --- a/src/database/migrations/2020_10_29_100000_add_beta_skus.php +++ /dev/null @@ -1,53 +0,0 @@ -first()) { - \App\Sku::create([ - 'title' => 'beta', - 'name' => 'Beta program', - 'description' => 'Access to beta program subscriptions', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta', - 'active' => false, - ]); - } - - if (!\App\Sku::where('title', 'meet')->first()) { - \App\Sku::create([ - 'title' => 'meet', - 'name' => 'Video chat', - 'description' => 'Video conferencing tool', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta\Meet', - 'active' => true, - ]); - } - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - // there's no need to remove these SKUs - } -} diff --git a/src/database/migrations/2021_04_22_120000_add_distlist_beta_sku.php b/src/database/migrations/2021_04_22_120000_add_distlist_beta_sku.php deleted file mode 100644 index e7ae98d3..00000000 --- a/src/database/migrations/2021_04_22_120000_add_distlist_beta_sku.php +++ /dev/null @@ -1,40 +0,0 @@ -first()) { - \App\Sku::create([ - 'title' => 'distlist', - 'name' => 'Distribution lists', - 'description' => 'Access to mail distribution lists', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Distlist', - 'active' => true, - ]); - } - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - // there's no need to remove this SKU - } -} diff --git a/src/database/migrations/2021_11_16_100000_create_resources_tables.php b/src/database/migrations/2021_11_16_100000_create_resources_tables.php index c6c47c22..3689e411 100644 --- a/src/database/migrations/2021_11_16_100000_create_resources_tables.php +++ b/src/database/migrations/2021_11_16_100000_create_resources_tables.php @@ -1,80 +1,62 @@ unsignedBigInteger('id'); $table->string('email')->unique(); $table->string('name'); $table->smallInteger('status'); $table->unsignedBigInteger('tenant_id')->nullable(); $table->timestamps(); $table->softDeletes(); $table->primary('id'); $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null'); } ); Schema::create( 'resource_settings', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('resource_id'); $table->string('key'); $table->text('value'); $table->timestamps(); $table->foreign('resource_id')->references('id')->on('resources') ->onDelete('cascade')->onUpdate('cascade'); $table->unique(['resource_id', 'key']); } ); - - \App\Sku::where('title', 'resource')->update([ - 'active' => true, - 'cost' => 0, - ]); - - if (!\App\Sku::where('title', 'beta-resources')->first()) { - \App\Sku::create([ - 'title' => 'beta-resources', - 'name' => 'Calendaring resources', - 'description' => 'Access to calendaring resources', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta\Resources', - 'active' => true, - ]); - } } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('resource_settings'); Schema::dropIfExists('resources'); // there's no need to remove the SKU } } diff --git a/src/database/migrations/2021_11_25_100000_create_shared_folders_table.php b/src/database/migrations/2021_11_25_100000_create_shared_folders_table.php index 01ce6f94..e79242f9 100644 --- a/src/database/migrations/2021_11_25_100000_create_shared_folders_table.php +++ b/src/database/migrations/2021_11_25_100000_create_shared_folders_table.php @@ -1,83 +1,64 @@ unsignedBigInteger('id'); $table->string('email')->unique(); $table->string('name'); $table->string('type', 8); $table->smallInteger('status'); $table->unsignedBigInteger('tenant_id')->nullable(); $table->timestamps(); $table->softDeletes(); $table->primary('id'); $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null'); } ); Schema::create( 'shared_folder_settings', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('shared_folder_id'); $table->string('key'); $table->text('value'); $table->timestamps(); $table->foreign('shared_folder_id')->references('id')->on('shared_folders') ->onDelete('cascade')->onUpdate('cascade'); $table->unique(['shared_folder_id', 'key']); } ); - - \App\Sku::where('title', 'shared_folder')->update([ - 'active' => true, - 'cost' => 0, - 'title' => 'shared-folder', - ]); - - if (!\App\Sku::where('title', 'beta-shared-folders')->first()) { - \App\Sku::create([ - 'title' => 'beta-shared-folders', - 'name' => 'Shared folders', - 'description' => 'Access to shared folders', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta\SharedFolders', - 'active' => true, - ]); - } } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('shared_folder_settings'); Schema::dropIfExists('shared_folders'); // there's no need to remove the SKU } } diff --git a/src/database/migrations/2022_03_02_100000_create_filesystem_tables.php b/src/database/migrations/2022_03_02_100000_create_filesystem_tables.php index 03314f35..e8c2e48d 100644 --- a/src/database/migrations/2022_03_02_100000_create_filesystem_tables.php +++ b/src/database/migrations/2022_03_02_100000_create_filesystem_tables.php @@ -1,91 +1,76 @@ string('id', 36)->primary(); $table->bigInteger('user_id')->index(); $table->integer('type')->unsigned()->default(0); $table->timestamps(); $table->softDeletes(); $table->foreign('user_id')->references('id')->on('users') ->onUpdate('cascade')->onDelete('cascade'); } ); Schema::create( 'fs_properties', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('item_id', 36); $table->string('key')->index(); $table->text('value'); $table->timestamps(); $table->unique(['item_id', 'key']); $table->foreign('item_id')->references('id')->on('fs_items') ->onDelete('cascade')->onUpdate('cascade'); } ); Schema::create( 'fs_chunks', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('item_id', 36); $table->string('chunk_id', 36); $table->integer('sequence')->default(0); $table->integer('size')->unsigned()->default(0); $table->timestamps(); $table->softDeletes(); $table->unique(['item_id', 'chunk_id']); // $table->unique(['item_id', 'sequence', 'deleted_at']); $table->foreign('item_id')->references('id')->on('fs_items') ->onUpdate('cascade')->onDelete('cascade'); } ); - - if (\config('app.with_files') && !\App\Sku::where('title', 'files')->first()) { - \App\Sku::create([ - 'title' => 'files', - 'name' => 'File storage', - 'description' => 'Access to file storage', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Files', - 'active' => true, - ]); - } } /** * Reverse the migrations. */ public function down(): void { - \App\Sku::where('title', 'files')->delete(); - Schema::dropIfExists('fs_properties'); Schema::dropIfExists('fs_chunks'); Schema::dropIfExists('fs_items'); } }; diff --git a/src/database/migrations/2022_06_03_100000_drop_beta_skus.php b/src/database/migrations/2022_06_03_100000_drop_beta_skus.php deleted file mode 100644 index cda5e82d..00000000 --- a/src/database/migrations/2022_06_03_100000_drop_beta_skus.php +++ /dev/null @@ -1,67 +0,0 @@ -delete(); - - \App\Sku::where('title', 'beta')->update(['description' => 'Access to the private beta program features']); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - \App\Sku::create([ - 'title' => 'beta-distlists', - 'name' => 'Distribution lists', - 'description' => 'Access to mail distribution lists', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta\Distlists', - 'active' => true, - ]); - \App\Sku::create([ - 'title' => 'beta-resources', - 'name' => 'Calendaring resources', - 'description' => 'Access to calendaring resources', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta\Resources', - 'active' => true, - ]); - \App\Sku::create([ - 'title' => 'beta-shared-folders', - 'name' => 'Shared folders', - 'description' => 'Access to shared folders', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Beta\SharedFolders', - 'active' => true, - ]); - \App\Sku::create([ - 'title' => 'files', - 'name' => 'File storage', - 'description' => 'Access to file storage', - 'cost' => 0, - 'units_free' => 0, - 'period' => 'monthly', - 'handler_class' => 'App\Handlers\Files', - 'active' => true, - ]); - } -};