diff --git a/src/app/Http/Controllers/API/V4/SearchController.php b/src/app/Http/Controllers/API/V4/SearchController.php new file mode 100644 index 00000000..4ec70ebf --- /dev/null +++ b/src/app/Http/Controllers/API/V4/SearchController.php @@ -0,0 +1,159 @@ +guard()->user(); + $search = trim(request()->input('search')); + $with_aliases = !empty(request()->input('alias')); + $limit = intval(request()->input('limit')); + + if ($limit <= 0) { + $limit = 15; + } elseif ($limit > 100) { + $limit = 100; + } + + // Prepare the query + $query = User::select('email', 'id')->where('id', $user->id); + $aliases = DB::table('user_aliases')->select(DB::raw('alias as email, user_id as id')) + ->where('user_id', $user->id); + + if (strlen($search)) { + $aliases->whereLike('alias', $search); + $query->whereLike('email', $search); + } + + if ($with_aliases) { + $query->union($aliases); + } + + // Execute the query + $result = $query->orderBy('email')->limit($limit)->get(); + + $result = $this->resultFormat($result); + + return response()->json([ + 'list' => $result, + 'count' => count($result), + ]); + } + + /** + * Search request for addresses of all users (in an account) + * + * @param \Illuminate\Http\Request $request The API request. + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function searchUser(Request $request) + { + $user = $this->guard()->user(); + $search = trim(request()->input('search')); + $with_aliases = !empty(request()->input('alias')); + $limit = intval(request()->input('limit')); + + if ($limit <= 0) { + $limit = 15; + } elseif ($limit > 100) { + $limit = 100; + } + + $wallet = $user->wallet(); + + // Limit users to the user's account + $allUsers = $wallet->entitlements()->where('entitleable_type', User::class)->select('entitleable_id')->distinct(); + + // Sub-query for user IDs who's names match the search criteria + $foundUserIds = UserSetting::select('user_id') + ->whereIn('key', ['first_name', 'last_name']) + ->whereLike('value', $search) + ->whereIn('user_id', $allUsers); + + // Prepare the query + $query = User::select('email', 'id')->whereIn('id', $allUsers); + $aliases = DB::table('user_aliases')->select(DB::raw('alias as email, user_id as id')) + ->whereIn('user_id', $allUsers); + + if (strlen($search)) { + $query->where(function ($query) use ($foundUserIds, $search) { + $query->whereLike('email', $search) + ->orWhereIn('id', $foundUserIds); + }); + + $aliases->where(function ($query) use ($foundUserIds, $search) { + $query->whereLike('alias', $search) + ->orWhereIn('user_id', $foundUserIds); + }); + } + + if ($with_aliases) { + $query->union($aliases); + } + + // Execute the query + $result = $query->orderBy('email')->limit($limit)->get(); + + $result = $this->resultFormat($result); + + return response()->json([ + 'list' => $result, + 'count' => count($result), + ]); + } + + /** + * Format the search result, inject user names + */ + protected function resultFormat($result) + { + if ($result->count()) { + // Get user names + $settings = UserSetting::whereIn('key', ['first_name', 'last_name']) + ->whereIn('user_id', $result->pluck('id')) + ->get() + ->mapWithKeys(function ($item) { + return [($item->user_id . ':' . $item->key) => $item->value]; + }) + ->all(); + + // "Format" the result, include user names + $result = $result->map(function ($record) use ($settings) { + return [ + 'email' => $record->email, + 'name' => trim( + ($settings["{$record->id}:first_name"] ?? '') + . ' ' + . ($settings["{$record->id}:last_name"] ?? '') + ), + ]; + }) + ->sortBy(['name', 'email']) + ->values(); + } + + return $result; + } +} diff --git a/src/config/app.php b/src/config/app.php index 864ed1c0..767653b3 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -1,280 +1,281 @@ env('APP_NAME', 'Laravel'), /* |-------------------------------------------------------------------------- | Application Environment |-------------------------------------------------------------------------- | | This value determines the "environment" your application is currently | running in. This may determine how you prefer to configure various | services the application utilizes. Set this in your ".env" file. | */ 'env' => env('APP_ENV', 'production'), /* |-------------------------------------------------------------------------- | Application Debug Mode |-------------------------------------------------------------------------- | | When your application is in debug mode, detailed error messages with | stack traces will be shown on every error that occurs within your | application. If disabled, a simple generic error page is shown. | */ 'debug' => (bool) env('APP_DEBUG', false), /* |-------------------------------------------------------------------------- | Application URL |-------------------------------------------------------------------------- | | This URL is used by the console to properly generate URLs when using | the Artisan command line tool. You should set this to the root of | your application so that it is used when running Artisan tasks. */ 'url' => env('APP_URL', 'http://localhost'), 'passphrase' => env('APP_PASSPHRASE', null), 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), 'asset_url' => env('ASSET_URL'), 'support_url' => env('SUPPORT_URL', null), 'support_email' => env('SUPPORT_EMAIL', null), 'webmail_url' => env('WEBMAIL_URL', null), 'theme' => env('APP_THEME', 'default'), 'tenant_id' => env('APP_TENANT_ID', null), 'currency' => \strtoupper(env('APP_CURRENCY', 'CHF')), /* |-------------------------------------------------------------------------- | Application Domain |-------------------------------------------------------------------------- | | System domain used for user signup (kolab identity) */ 'domain' => env('APP_DOMAIN', 'domain.tld'), 'website_domain' => env('APP_WEBSITE_DOMAIN', env('APP_DOMAIN', 'domain.tld')), // Restrict over which domains the services paths can be accessed. 'services_allowed_domains' => explode(',', env( 'APP_SERVICES_ALLOWED_DOMAINS', "webapp,kolab," . env( 'APP_SERVICES_DOMAIN', "services." . env( 'APP_WEBSITE_DOMAIN', env('APP_DOMAIN', 'domain.tld') ) ) )), /* |-------------------------------------------------------------------------- | Application Timezone |-------------------------------------------------------------------------- | | Here you may specify the default timezone for your application, which | will be used by the PHP date and date-time functions. We have gone | ahead and set this to a sensible default for you out of the box. | */ 'timezone' => 'UTC', /* |-------------------------------------------------------------------------- | Application Locale Configuration |-------------------------------------------------------------------------- | | The application locale determines the default locale that will be used | by the translation service provider. You are free to set this value | to any of the locales which will be supported by the application. | */ 'locale' => env('APP_LOCALE', 'en'), /* |-------------------------------------------------------------------------- | Application Fallback Locale |-------------------------------------------------------------------------- | | The fallback locale determines the locale to use when the current one | is not available. You may change the value to correspond to any of | the language folders that are provided through your application. | */ 'fallback_locale' => 'en', /* |-------------------------------------------------------------------------- | Faker Locale |-------------------------------------------------------------------------- | | This locale will be used by the Faker PHP library when generating fake | data for your database seeds. For example, this will be used to get | localized telephone numbers, street address information and more. | */ 'faker_locale' => 'en_US', /* |-------------------------------------------------------------------------- | Encryption Key |-------------------------------------------------------------------------- | | This key is used by the Illuminate encrypter service and should be set | to a random, 32 character string, otherwise these encrypted strings | will not be safe. Please do this before deploying an application! | */ 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', /* |-------------------------------------------------------------------------- | Autoloaded Service Providers |-------------------------------------------------------------------------- | | The service providers listed here will be automatically loaded on the | request to your application. Feel free to add your own services to | this array to grant expanded functionality to your applications. | */ 'providers' => ServiceProvider::defaultProviders()->merge([ /* * Application Service Providers... */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\PassportServiceProvider::class, App\Providers\RouteServiceProvider::class, ])->toArray(), /* |-------------------------------------------------------------------------- | Class Aliases |-------------------------------------------------------------------------- | | This array of class aliases will be registered when this application | is started. However, feel free to register as many as you wish as | the aliases are "lazy" loaded so they don't hinder performance. | */ 'aliases' => Facade::defaultAliases()->toArray(), 'headers' => [ 'csp' => env('APP_HEADER_CSP', ""), 'xfo' => env('APP_HEADER_XFO', ""), ], // Locations of knowledge base articles 'kb' => [ // An article about suspended accounts 'account_suspended' => env('KB_ACCOUNT_SUSPENDED'), // An article about a way to delete an owned account 'account_delete' => env('KB_ACCOUNT_DELETE'), // An article about the payment system 'payment_system' => env('KB_PAYMENT_SYSTEM'), ], 'company' => [ 'name' => env('COMPANY_NAME'), 'address' => env('COMPANY_ADDRESS'), 'details' => env('COMPANY_DETAILS'), 'email' => env('COMPANY_EMAIL'), 'logo' => env('COMPANY_LOGO'), 'footer' => env('COMPANY_FOOTER', env('COMPANY_DETAILS')), 'copyright' => 'Apheleia IT AG', ], 'storage' => [ 'min_qty' => (int) env('STORAGE_MIN_QTY', 5), // in GB ], 'vat' => [ 'mode' => (int) env('VAT_MODE', 0), ], 'password_policy' => env('PASSWORD_POLICY') ?: 'min:6,max:255', 'payment' => [ 'methods_oneoff' => env('PAYMENT_METHODS_ONEOFF', 'creditcard,paypal,banktransfer,bitcoin'), 'methods_recurring' => env('PAYMENT_METHODS_RECURRING', 'creditcard'), ], 'with_ldap' => (bool) env('APP_LDAP', true), 'with_imap' => (bool) env('APP_IMAP', false), 'with_admin' => (bool) env('APP_WITH_ADMIN', false), 'with_files' => (bool) env('APP_WITH_FILES', false), 'with_reseller' => (bool) env('APP_WITH_RESELLER', false), 'with_services' => (bool) env('APP_WITH_SERVICES', false), 'with_signup' => (bool) env('APP_WITH_SIGNUP', true), 'with_subscriptions' => (bool) env('APP_WITH_SUBSCRIPTIONS', true), 'with_wallet' => (bool) env('APP_WITH_WALLET', true), 'with_distlists' => (bool) env('APP_WITH_DISTLISTS', true), 'with_shared_folders' => (bool) env('APP_WITH_SHARED_FOLDERS', true), 'with_resources' => (bool) env('APP_WITH_RESOURCES', true), 'with_meet' => (bool) env('APP_WITH_MEET', true), 'with_companion_app' => (bool) env('APP_WITH_COMPANION_APP', true), + 'with_user_search' => (bool) env('APP_WITH_USER_SEARCH', false), 'signup' => [ 'email_limit' => (int) env('SIGNUP_LIMIT_EMAIL', 0), 'ip_limit' => (int) env('SIGNUP_LIMIT_IP', 0), ], 'woat_ns1' => env('WOAT_NS1', 'ns01.' . env('APP_DOMAIN')), 'woat_ns2' => env('WOAT_NS2', 'ns02.' . env('APP_DOMAIN')), 'ratelimit_whitelist' => explode(',', env('RATELIMIT_WHITELIST', '')), 'companion_download_link' => env( 'COMPANION_DOWNLOAD_LINK', "https://mirror.apheleia-it.ch/pub/companion-app-beta.apk" ), 'vpn' => [ 'token_signing_key' => env('VPN_TOKEN_SIGNING_KEY', 0), ], 'mta_sts' => env('MTA_STS') ]; diff --git a/src/routes/api.php b/src/routes/api.php index 4f460fea..3460b961 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -1,332 +1,337 @@ 'api', 'prefix' => 'auth' ], function () { Route::post('login', [API\AuthController::class, 'login']); Route::group( ['middleware' => 'auth:api'], function () { Route::get('info', [API\AuthController::class, 'info']); Route::post('info', [API\AuthController::class, 'info']); Route::get('location', [API\AuthController::class, 'location']); Route::post('logout', [API\AuthController::class, 'logout']); Route::post('refresh', [API\AuthController::class, 'refresh']); } ); } ); Route::group( [ 'domain' => \config('app.website_domain'), 'middleware' => 'api', 'prefix' => 'auth' ], function () { Route::post('password-policy/check', [API\PasswordPolicyController::class, 'check']); Route::post('password-reset/init', [API\PasswordResetController::class, 'init']); Route::post('password-reset/verify', [API\PasswordResetController::class, 'verify']); Route::post('password-reset', [API\PasswordResetController::class, 'reset']); } ); if (\config('app.with_signup')) { Route::group( [ 'domain' => \config('app.website_domain'), 'middleware' => 'api', 'prefix' => 'auth' ], function () { Route::get('signup/domains', [API\SignupController::class, 'domains']); Route::post('signup/init', [API\SignupController::class, 'init']); Route::get('signup/invitations/{id}', [API\SignupController::class, 'invitation']); Route::get('signup/plans', [API\SignupController::class, 'plans']); Route::post('signup/validate', [API\SignupController::class, 'signupValidate']); Route::post('signup/verify', [API\SignupController::class, 'verify']); Route::post('signup', [API\SignupController::class, 'signup']); } ); } Route::group( [ 'domain' => \config('app.website_domain'), 'middleware' => ['auth:api', 'scope:mfa,api'], 'prefix' => 'v4' ], function () { Route::post('auth-attempts/{id}/confirm', [API\V4\AuthAttemptsController::class, 'confirm']); Route::post('auth-attempts/{id}/deny', [API\V4\AuthAttemptsController::class, 'deny']); Route::get('auth-attempts/{id}/details', [API\V4\AuthAttemptsController::class, 'details']); Route::get('auth-attempts', [API\V4\AuthAttemptsController::class, 'index']); Route::post('companion/register', [API\V4\CompanionAppsController::class, 'register']); } ); if (\config('app.with_files')) { Route::group( [ 'middleware' => ['auth:api', 'scope:fs,api'], 'prefix' => 'v4' ], function () { Route::apiResource('fs', API\V4\FsController::class); Route::get('fs/{itemId}/permissions', [API\V4\FsController::class, 'getPermissions']); Route::post('fs/{itemId}/permissions', [API\V4\FsController::class, 'createPermission']); Route::put('fs/{itemId}/permissions/{id}', [API\V4\FsController::class, 'updatePermission']); Route::delete('fs/{itemId}/permissions/{id}', [API\V4\FsController::class, 'deletePermission']); } ); Route::group( [ 'middleware' => [], 'prefix' => 'v4' ], function () { Route::post('fs/uploads/{id}', [API\V4\FsController::class, 'upload']) ->middleware(['api']); Route::get('fs/downloads/{id}', [API\V4\FsController::class, 'download']); } ); } Route::group( [ 'domain' => \config('app.website_domain'), 'middleware' => ['auth:api', 'scope:api'], 'prefix' => 'v4' ], function () { Route::apiResource('companions', API\V4\CompanionAppsController::class); // This must not be accessible with the 2fa token, // to prevent an attacker from pairing a new device with a stolen token. Route::get('companions/{id}/pairing', [API\V4\CompanionAppsController::class, 'pairing']); Route::apiResource('domains', API\V4\DomainsController::class); Route::get('domains/{id}/confirm', [API\V4\DomainsController::class, 'confirm']); Route::get('domains/{id}/skus', [API\V4\DomainsController::class, 'skus']); Route::get('domains/{id}/status', [API\V4\DomainsController::class, 'status']); Route::post('domains/{id}/config', [API\V4\DomainsController::class, 'setConfig']); Route::apiResource('groups', API\V4\GroupsController::class); Route::get('groups/{id}/skus', [API\V4\GroupsController::class, 'skus']); Route::get('groups/{id}/status', [API\V4\GroupsController::class, 'status']); Route::post('groups/{id}/config', [API\V4\GroupsController::class, 'setConfig']); Route::apiResource('packages', API\V4\PackagesController::class); Route::apiResource('rooms', API\V4\RoomsController::class); Route::post('rooms/{id}/config', [API\V4\RoomsController::class, 'setConfig']); Route::get('rooms/{id}/skus', [API\V4\RoomsController::class, 'skus']); Route::post('meet/rooms/{id}', [API\V4\MeetController::class, 'joinRoom']) ->withoutMiddleware(['auth:api', 'scope:api']); Route::apiResource('resources', API\V4\ResourcesController::class); Route::get('resources/{id}/skus', [API\V4\ResourcesController::class, 'skus']); Route::get('resources/{id}/status', [API\V4\ResourcesController::class, 'status']); Route::post('resources/{id}/config', [API\V4\ResourcesController::class, 'setConfig']); Route::apiResource('shared-folders', API\V4\SharedFoldersController::class); Route::get('shared-folders/{id}/skus', [API\V4\SharedFoldersController::class, 'skus']); Route::get('shared-folders/{id}/status', [API\V4\SharedFoldersController::class, 'status']); Route::post('shared-folders/{id}/config', [API\V4\SharedFoldersController::class, 'setConfig']); Route::apiResource('skus', API\V4\SkusController::class); Route::apiResource('users', API\V4\UsersController::class); Route::post('users/{id}/config', [API\V4\UsersController::class, 'setConfig']); Route::get('users/{id}/skus', [API\V4\UsersController::class, 'skus']); Route::get('users/{id}/status', [API\V4\UsersController::class, 'status']); Route::apiResource('wallets', API\V4\WalletsController::class); Route::get('wallets/{id}/transactions', [API\V4\WalletsController::class, 'transactions']); Route::get('wallets/{id}/receipts', [API\V4\WalletsController::class, 'receipts']); Route::get('wallets/{id}/receipts/{receipt}', [API\V4\WalletsController::class, 'receiptDownload']); Route::get('password-policy', [API\PasswordPolicyController::class, 'index']); Route::post('password-reset/code', [API\PasswordResetController::class, 'codeCreate']); Route::delete('password-reset/code/{id}', [API\PasswordResetController::class, 'codeDelete']); Route::post('payments', [API\V4\PaymentsController::class, 'store']); //Route::delete('payments', [API\V4\PaymentsController::class, 'cancel']); Route::get('payments/mandate', [API\V4\PaymentsController::class, 'mandate']); Route::post('payments/mandate', [API\V4\PaymentsController::class, 'mandateCreate']); Route::put('payments/mandate', [API\V4\PaymentsController::class, 'mandateUpdate']); Route::delete('payments/mandate', [API\V4\PaymentsController::class, 'mandateDelete']); Route::post('payments/mandate/reset', [API\V4\PaymentsController::class, 'mandateReset']); Route::get('payments/methods', [API\V4\PaymentsController::class, 'paymentMethods']); Route::get('payments/pending', [API\V4\PaymentsController::class, 'payments']); Route::get('payments/has-pending', [API\V4\PaymentsController::class, 'hasPayments']); Route::get('payments/status', [API\V4\PaymentsController::class, 'paymentStatus']); + Route::get('search/self', [API\V4\SearchController::class, 'searchSelf']); + if (\config('app.with_user_search')) { + Route::get('search/user', [API\V4\SearchController::class, 'searchUser']); + } + Route::post('support/request', [API\V4\SupportController::class, 'request']) ->withoutMiddleware(['auth:api', 'scope:api']) ->middleware(['api']); Route::get('vpn/token', [API\V4\VPNController::class, 'token']); } ); Route::group( [ 'domain' => \config('app.website_domain'), 'prefix' => 'webhooks' ], function () { Route::post('payment/{provider}', [API\V4\PaymentsController::class, 'webhook']); Route::post('meet', [API\V4\MeetController::class, 'webhook']); } ); if (\config('app.with_services')) { Route::group( [ 'middleware' => ['allowedHosts'], 'prefix' => 'webhooks' ], function () { Route::get('nginx', [API\V4\NGINXController::class, 'authenticate']); Route::get('nginx-roundcube', [API\V4\NGINXController::class, 'authenticateRoundcube']); Route::get('nginx-httpauth', [API\V4\NGINXController::class, 'httpauth']); Route::post('cyrus-sasl', [API\V4\NGINXController::class, 'cyrussasl']); Route::post('policy/greylist', [API\V4\PolicyController::class, 'greylist']); Route::post('policy/ratelimit', [API\V4\PolicyController::class, 'ratelimit']); Route::post('policy/spf', [API\V4\PolicyController::class, 'senderPolicyFramework']); } ); } Route::get('health/readiness', [API\V4\HealthController::class, 'readiness']); Route::get('health/liveness', [API\V4\HealthController::class, 'liveness']); if (\config('app.with_admin')) { Route::group( [ 'domain' => 'admin.' . \config('app.website_domain'), 'middleware' => ['auth:api', 'admin'], 'prefix' => 'v4', ], function () { Route::apiResource('domains', API\V4\Admin\DomainsController::class); Route::get('domains/{id}/skus', [API\V4\Admin\DomainsController::class, 'skus']); Route::post('domains/{id}/suspend', [API\V4\Admin\DomainsController::class, 'suspend']); Route::post('domains/{id}/unsuspend', [API\V4\Admin\DomainsController::class, 'unsuspend']); Route::get('eventlog/{type}/{id}', [API\V4\Admin\EventLogController::class, 'index']); Route::apiResource('groups', API\V4\Admin\GroupsController::class); Route::post('groups/{id}/suspend', [API\V4\Admin\GroupsController::class, 'suspend']); Route::post('groups/{id}/unsuspend', [API\V4\Admin\GroupsController::class, 'unsuspend']); Route::apiResource('resources', API\V4\Admin\ResourcesController::class); Route::apiResource('shared-folders', API\V4\Admin\SharedFoldersController::class); Route::apiResource('skus', API\V4\Admin\SkusController::class); Route::apiResource('users', API\V4\Admin\UsersController::class); Route::get('users/{id}/discounts', [API\V4\Admin\DiscountsController::class, 'userDiscounts']); Route::post('users/{id}/reset2FA', [API\V4\Admin\UsersController::class, 'reset2FA']); Route::post('users/{id}/resetGeoLock', [API\V4\Admin\UsersController::class, 'resetGeoLock']); Route::post('users/{id}/resync', [API\V4\Admin\UsersController::class, 'resync']); Route::get('users/{id}/skus', [API\V4\Admin\UsersController::class, 'skus']); Route::post('users/{id}/skus/{sku}', [API\V4\Admin\UsersController::class, 'setSku']); Route::post('users/{id}/suspend', [API\V4\Admin\UsersController::class, 'suspend']); Route::post('users/{id}/unsuspend', [API\V4\Admin\UsersController::class, 'unsuspend']); Route::apiResource('wallets', API\V4\Admin\WalletsController::class); Route::post('wallets/{id}/one-off', [API\V4\Admin\WalletsController::class, 'oneOff']); Route::get('wallets/{id}/transactions', [API\V4\Admin\WalletsController::class, 'transactions']); Route::get('stats/chart/{chart}', [API\V4\Admin\StatsController::class, 'chart']); } ); Route::group( [ 'domain' => 'admin.' . \config('app.website_domain'), 'prefix' => 'v4', ], function () { Route::get('inspect-request', [API\V4\Admin\UsersController::class, 'inspectRequest']); } ); } if (\config('app.with_reseller')) { Route::group( [ 'domain' => 'reseller.' . \config('app.website_domain'), 'middleware' => ['auth:api', 'reseller'], 'prefix' => 'v4', ], function () { Route::apiResource('domains', API\V4\Reseller\DomainsController::class); Route::get('domains/{id}/skus', [API\V4\Reseller\DomainsController::class, 'skus']); Route::post('domains/{id}/suspend', [API\V4\Reseller\DomainsController::class, 'suspend']); Route::post('domains/{id}/unsuspend', [API\V4\Reseller\DomainsController::class, 'unsuspend']); Route::get('eventlog/{type}/{id}', [API\V4\Reseller\EventLogController::class, 'index']); Route::apiResource('groups', API\V4\Reseller\GroupsController::class); Route::post('groups/{id}/suspend', [API\V4\Reseller\GroupsController::class, 'suspend']); Route::post('groups/{id}/unsuspend', [API\V4\Reseller\GroupsController::class, 'unsuspend']); Route::apiResource('invitations', API\V4\Reseller\InvitationsController::class); Route::post('invitations/{id}/resend', [API\V4\Reseller\InvitationsController::class, 'resend']); Route::post('payments', [API\V4\Reseller\PaymentsController::class, 'store']); Route::get('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandate']); Route::post('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateCreate']); Route::put('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateUpdate']); Route::delete('payments/mandate', [API\V4\Reseller\PaymentsController::class, 'mandateDelete']); Route::get('payments/methods', [API\V4\Reseller\PaymentsController::class, 'paymentMethods']); Route::get('payments/pending', [API\V4\Reseller\PaymentsController::class, 'payments']); Route::get('payments/has-pending', [API\V4\Reseller\PaymentsController::class, 'hasPayments']); Route::apiResource('resources', API\V4\Reseller\ResourcesController::class); Route::apiResource('shared-folders', API\V4\Reseller\SharedFoldersController::class); Route::apiResource('skus', API\V4\Reseller\SkusController::class); Route::apiResource('users', API\V4\Reseller\UsersController::class); Route::get('users/{id}/discounts', [API\V4\Reseller\DiscountsController::class, 'userDiscounts']); Route::post('users/{id}/reset2FA', [API\V4\Reseller\UsersController::class, 'reset2FA']); Route::post('users/{id}/resetGeoLock', [API\V4\Reseller\UsersController::class, 'resetGeoLock']); Route::post('users/{id}/resync', [API\V4\Reseller\UsersController::class, 'resync']); Route::get('users/{id}/skus', [API\V4\Reseller\UsersController::class, 'skus']); Route::post('users/{id}/skus/{sku}', [API\V4\Reseller\UsersController::class, 'setSku']); Route::post('users/{id}/suspend', [API\V4\Reseller\UsersController::class, 'suspend']); Route::post('users/{id}/unsuspend', [API\V4\Reseller\UsersController::class, 'unsuspend']); Route::apiResource('wallets', API\V4\Reseller\WalletsController::class); Route::post('wallets/{id}/one-off', [API\V4\Reseller\WalletsController::class, 'oneOff']); Route::get('wallets/{id}/receipts', [API\V4\Reseller\WalletsController::class, 'receipts']); Route::get('wallets/{id}/receipts/{receipt}', [API\V4\Reseller\WalletsController::class, 'receiptDownload']); Route::get('wallets/{id}/transactions', [API\V4\Reseller\WalletsController::class, 'transactions']); Route::get('stats/chart/{chart}', [API\V4\Reseller\StatsController::class, 'chart']); } ); } diff --git a/src/tests/Feature/Controller/SearchTest.php b/src/tests/Feature/Controller/SearchTest.php new file mode 100644 index 00000000..d7459afa --- /dev/null +++ b/src/tests/Feature/Controller/SearchTest.php @@ -0,0 +1,192 @@ +deleteTestUser('jane@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('jane@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test searching + */ + public function testSearchSelf(): void + { + // Unauth access not allowed + $response = $this->get("api/v4/search/self"); + $response->assertStatus(401); + + $john = $this->getTestUser('john@kolab.org'); + $jack = $this->getTestUser('jack@kolab.org'); + + // w/o aliases, w/o search + $response = $this->actingAs($john)->get("api/v4/search/self"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame(['email' => 'john@kolab.org', 'name' => 'John Doe'], $json['list'][0]); + + // with aliases, w/o search + $response = $this->actingAs($john)->get("api/v4/search/self?alias=1"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(2, $json['count']); + $this->assertCount(2, $json['list']); + $this->assertSame(['email' => 'john.doe@kolab.org', 'name' => 'John Doe'], $json['list'][0]); + $this->assertSame(['email' => 'john@kolab.org', 'name' => 'John Doe'], $json['list'][1]); + + // with aliases and search + $response = $this->actingAs($john)->get("api/v4/search/self?alias=1&search=doe@"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame(['email' => 'john.doe@kolab.org', 'name' => 'John Doe'], $json['list'][0]); + + // User no account owner - with aliases, w/o search + $response = $this->actingAs($jack)->get("api/v4/search/self?alias=1"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(2, $json['count']); + $this->assertCount(2, $json['list']); + $this->assertSame(['email' => 'jack.daniels@kolab.org', 'name' => 'Jack Daniels'], $json['list'][0]); + $this->assertSame(['email' => 'jack@kolab.org', 'name' => 'Jack Daniels'], $json['list'][1]); + } + + /** + * Test searching + */ + public function testSearchUser(): void + { + \putenv('APP_WITH_USER_SEARCH=false'); // can't be done using \config() + $this->refreshApplication(); // reload routes + + // User search route disabled + $response = $this->get("api/v4/search/user"); + $response->assertStatus(404); + + \putenv('APP_WITH_USER_SEARCH=true'); // can't be done using \config() + $this->refreshApplication(); // reload routes + + // Unauth access not allowed + $response = $this->get("api/v4/search/user"); + $response->assertStatus(401); + + $john = $this->getTestUser('john@kolab.org'); + $jack = $this->getTestUser('jack@kolab.org'); + $jane = $this->getTestUser('jane@kolabnow.com'); + + // Account owner - without aliases, w/o search + $response = $this->actingAs($john)->get("api/v4/search/user"); + $response->assertStatus(200); + + $json = $response->json(); + + $users = [ + [ + 'email' => 'joe@kolab.org', + 'name' => '', + ], + [ + 'email' => 'ned@kolab.org', + 'name' => 'Edward Flanders', + ], + [ + 'email' => 'jack@kolab.org', + 'name' => 'Jack Daniels', + ], + [ + 'email' => 'john@kolab.org', + 'name' => 'John Doe', + ], + ]; + + + $this->assertSame(count($users), $json['count']); + $this->assertSame($users, $json['list']); + + // User no account owner, without aliases w/o search + $response = $this->actingAs($jack)->get("api/v4/search/user"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(count($users), $json['count']); + $this->assertSame($users, $json['list']); + + // with aliases, w/o search + $response = $this->actingAs($john)->get("api/v4/search/user?alias=1"); + $response->assertStatus(200); + + $json = $response->json(); + + $expected = [ + [ + 'email' => 'joe.monster@kolab.org', + 'name' => '', + ], + $users[0], + $users[1], + [ + 'email' => 'jack.daniels@kolab.org', + 'name' => 'Jack Daniels', + ], + $users[2], + [ + 'email' => 'john.doe@kolab.org', + 'name' => 'John Doe', + ], + $users[3], + ]; + + $this->assertSame(count($expected), $json['count']); + $this->assertSame($expected, $json['list']); + + // with aliases and search + $response = $this->actingAs($john)->get("api/v4/search/user?alias=1&search=john"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(2, $json['count']); + $this->assertCount(2, $json['list']); + $this->assertSame(['email' => 'john.doe@kolab.org', 'name' => 'John Doe'], $json['list'][0]); + $this->assertSame(['email' => 'john@kolab.org', 'name' => 'John Doe'], $json['list'][1]); + + // Make sure we can't find users from outside of an account + $response = $this->actingAs($john)->get("api/v4/search/user?alias=1&search=jane"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(0, $json['count']); + } +}