diff --git a/src/app/Http/Controllers/MeetController.php b/src/app/Http/Controllers/MeetController.php new file mode 100644 index 00000000..5ff3b1e3 --- /dev/null +++ b/src/app/Http/Controllers/MeetController.php @@ -0,0 +1,53 @@ +with('env', \App\Utils::uiEnv()); + } + + public function room($id) + { + return view('meet.room', ['room' => $id])->with('env', \App\Utils::uiEnv()); + } + + /** + * Common error response builder for API (JSON) responses + * + * @param int $code Error code + * @param string $message Error message + * + * @return \Illuminate\Http\JsonResponse + */ + protected function errorResponse(int $code, string $message = null) + { + $errors = [ + 400 => "Bad request", + 401 => "Unauthorized", + 403 => "Access denied", + 404 => "Not found", + 422 => "Input validation error", + 405 => "Method not allowed", + 500 => "Internal server error", + ]; + + $response = [ + 'status' => 'error', + 'message' => $message ?: (isset($errors[$code]) ? $errors[$code] : "Server error"), + ]; + + return response()->json($response, $code); + } +} diff --git a/src/app/Utils.php b/src/app/Utils.php index 0a9f197c..6b78981a 100644 --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -1,205 +1,206 @@ diffInDays($end) + 1; } /** * Provide all unique combinations of elements in $input, with order and duplicates irrelevant. * * @param array $input The input array of elements. * * @return array[] */ public static function powerSet(array $input): array { $output = []; for ($x = 0; $x < count($input); $x++) { self::combine($input, $x + 1, 0, [], 0, $output); } return $output; } /** * Returns the current user's email address or null. * * @return string */ public static function userEmailOrNull(): ?string { $user = Auth::user(); if (!$user) { return null; } return $user->email; } /** * Returns a random string consisting of a quantity of segments of a certain length joined. * * Example: * * ```php * $roomName = strtolower(\App\Utils::randStr(3, 3, '-'); * // $roomName == '3qb-7cs-cjj' * ``` * * @param int $length The length of each segment * @param int $qty The quantity of segments * @param string $join The string to use to join the segments * * @return string */ public static function randStr($length, $qty = 1, $join = '') { $chars = env('SHORTCODE_CHARS', self::CHARS); $randStrs = []; for ($x = 0; $x < $qty; $x++) { $randStrs[$x] = []; for ($y = 0; $y < $length; $y++) { $randStrs[$x][] = $chars[rand(0, strlen($chars) - 1)]; } shuffle($randStrs[$x]); $randStrs[$x] = implode('', $randStrs[$x]); } return implode($join, $randStrs); } /** * Returns a UUID in the form of an integer. * * @return integer */ public static function uuidInt(): int { $hex = Uuid::uuid4(); $bin = pack('h*', str_replace('-', '', $hex)); $ids = unpack('L', $bin); $id = array_shift($ids); return $id; } /** * Returns a UUID in the form of a string. * * @return string */ public static function uuidStr(): string { return Uuid::uuid4()->toString(); } private static function combine($input, $r, $index, $data, $i, &$output): void { $n = count($input); // Current cobination is ready if ($index == $r) { $output[] = array_slice($data, 0, $r); return; } // When no more elements are there to put in data[] if ($i >= $n) { return; } // current is included, put next at next location $data[$index] = $input[$i]; self::combine($input, $r, $index + 1, $data, $i + 1, $output); // current is excluded, replace it with next (Note that i+1 // is passed, but index is not changed) self::combine($input, $r, $index, $data, $i + 1, $output); } /** * Create self URL * * @param string $route Route/Path + * @todo Move this to App\Http\Controllers\Controller * * @return string Full URL */ public static function serviceUrl(string $route): string { $url = \url($route); $app_url = trim(\config('app.url'), '/'); $pub_url = trim(\config('app.public_url'), '/'); if ($pub_url != $app_url) { $url = str_replace($app_url, $pub_url, $url); } return $url; } /** * Create a configuration/environment data to be passed to * the UI * * @todo Move this to App\Http\Controllers\Controller * * @return array Configuration data */ public static function uiEnv(): array { $countries = include resource_path('countries.php'); $req_domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost()); $sys_domain = \config('app.domain'); $path = request()->path(); $opts = ['app.name', 'app.url', 'app.domain']; $env = \app('config')->getMany($opts); $env['countries'] = $countries ?: []; $env['view'] = 'root'; $env['jsapp'] = 'user.js'; if ($path == 'meet' || strpos($path, 'meet/') === 0) { $env['view'] = 'meet'; $env['jsapp'] = 'meet.js'; } elseif ($req_domain == "admin.$sys_domain") { $env['jsapp'] = 'admin.js'; } $env['paymentProvider'] = \config('services.payment_provider'); $env['stripePK'] = \config('services.stripe.public_key'); return $env; } } diff --git a/src/database/seeds/OpenViduRoomSeeder.php b/src/database/seeds/OpenViduRoomSeeder.php new file mode 100644 index 00000000..f4eef6d2 --- /dev/null +++ b/src/database/seeds/OpenViduRoomSeeder.php @@ -0,0 +1,33 @@ +first(); + $jack = \App\User::where('email', 'jack@kolab.org')->first(); + + \App\OpenVidu\Room::create( + [ + 'user_id' => $john->id, + 'name' => 'john' + ] + ); + + \App\OpenVidu\Room::create( + [ + 'user_id' => $jack->id, + 'name' => strtolower(\App\Utils::randStr(3, 3, '-')) + ] + ); + } +} diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss index abc7a5cc..4d3e80c0 100644 --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -1,353 +1,370 @@ @import 'variables'; @import 'bootstrap'; @import 'meet'; @import 'menu'; @import 'toast'; @import 'forms'; html, body, body > .outer-container { height: 100%; } #app { display: flex; flex-direction: column; min-height: 100%; & > nav { flex-shrink: 0; z-index: 12; } & > div.container { flex-grow: 1; margin-top: 2rem; margin-bottom: 2rem; } & > .filler { flex-grow: 1; } & > div.container + .filler { display: none; } } #error-page { position: absolute; top: 0; height: 100%; width: 100%; align-items: center; display: flex; justify-content: center; color: #636b6f; z-index: 10; background: white; .code { text-align: right; border-right: 2px solid; font-size: 26px; padding: 0 15px; } .message { font-size: 18px; padding: 0 15px; } } .app-loader { background-color: $body-bg; height: 100%; width: 100%; position: absolute; top: 0; left: 0; display: flex; align-items: center; justify-content: center; z-index: 8; .spinner-border { width: 120px; height: 120px; border-width: 15px; color: #b2aa99; } &.small .spinner-border { width: 25px; height: 25px; border-width: 3px; } &.fadeOut { visibility: hidden; opacity: 0; transition: visibility 400ms linear, opacity 400ms linear; } } pre { margin: 1rem 0; padding: 1rem; background-color: $menu-bg-color; } .card-title { font-size: 1.2rem; font-weight: bold; } tfoot.table-fake-body { background-color: #f8f8f8; color: grey; text-align: center; td { vertical-align: middle; height: 8em; } tbody:not(:empty) + & { display: none; } } table { td.buttons, td.email, td.price, td.datetime, td.selection { width: 1%; white-space: nowrap; } th.price, td.price { width: 1%; text-align: right; white-space: nowrap; } &.form-list { margin: 0; td { border: 0; &:first-child { padding-left: 0; } &:last-child { padding-right: 0; } } button { line-height: 1; } } .btn-action { line-height: 1; padding: 0; } } .list-details { min-height: 1em; & > ul { margin: 0; padding-left: 1.2em; } } .plan-selector { .plan-ico { font-size: 3.8rem; color: #f1a539; border: 3px solid #f1a539; width: 6rem; height: 6rem; margin-bottom: 1rem; border-radius: 50%; } } .plan-description { & > ul { padding-left: 1.2em; &:last-child { margin-bottom: 0; } } } #status-box { background-color: lighten($green, 35); .progress { background-color: #fff; height: 10px; } .progress-label { font-size: 0.9em; } .progress-bar { background-color: $green; } &.process-failed { background-color: lighten($orange, 30); .progress-bar { background-color: $red; } } } #dashboard-nav { display: flex; flex-wrap: wrap; justify-content: center; & > a { padding: 1rem; text-align: center; white-space: nowrap; margin: 0.25rem; text-decoration: none; width: 150px; &.disabled { pointer-events: none; opacity: 0.6; } .badge { position: absolute; top: 0.5rem; right: 0.5rem; } } svg { width: 6rem; height: 6rem; margin: auto; } } .form-separator { position: relative; margin: 1em 0; display: flex; justify-content: center; hr { border-color: #999; margin: 0; position: absolute; top: 0.75em; width: 100%; } span { background: #fff; padding: 0 1em; z-index: 1; } } // Various improvements for mobile @include media-breakpoint-down(sm) { .card { border: 0; } .card-body { padding: 0.5rem 0; } .form-group { margin-bottom: 0.5rem; } .tab-content { margin-top: 0.5rem; } .col-form-label { color: #666; font-size: 95%; } .form-group.plaintext .col-form-label { padding-bottom: 0; } form.read-only.short label { width: 35%; & + * { width: 65%; } } #app > div.container { margin-bottom: 1rem; margin-top: 1rem; } #header-menu-navbar { padding: 0; } #dashboard-nav > a { width: 135px; } .table-sm:not(.form-list) { tbody td { padding: 0.75rem 0.5rem; svg { vertical-align: -0.175em; } & > svg { font-size: 125%; margin-right: 0.25rem; } } } } // Openvidu webcomponent improvements mat-sidenav-container { z-index: 10 !important; } #dialogChooseRoom { top: 0; left: 0; z-index: 10; background: #fff; & > .mat-card { max-height: unset; margin: 2em auto; } } + +// Openvidu webcomponent improvements +mat-sidenav-container { + z-index: 10 !important; +} + +#dialogChooseRoom { + top: 0; + left: 0; + z-index: 10; + background: #fff; + + & > .mat-card { + max-height: unset; + margin: 2em auto; + } +} diff --git a/src/routes/api.php b/src/routes/api.php index e1c25970..e948b044 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -1,125 +1,136 @@ 'api', 'prefix' => 'auth' ], function ($router) { Route::post('login', 'API\AuthController@login'); Route::group( ['middleware' => 'auth:api'], function ($router) { Route::get('info', 'API\AuthController@info'); Route::post('logout', 'API\AuthController@logout'); Route::post('refresh', 'API\AuthController@refresh'); } ); } ); Route::group( [ 'domain' => \config('app.domain'), 'middleware' => 'api', 'prefix' => 'auth' ], function ($router) { Route::post('password-reset/init', 'API\PasswordResetController@init'); Route::post('password-reset/verify', 'API\PasswordResetController@verify'); Route::post('password-reset', 'API\PasswordResetController@reset'); Route::get('signup/plans', 'API\SignupController@plans'); Route::post('signup/init', 'API\SignupController@init'); Route::post('signup/verify', 'API\SignupController@verify'); Route::post('signup', 'API\SignupController@signup'); } ); Route::group( [ 'domain' => \config('app.domain'), 'middleware' => 'auth:api', 'prefix' => 'v4' ], function () { Route::apiResource('domains', API\V4\DomainsController::class); Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm'); Route::get('domains/{id}/status', 'API\V4\DomainsController@status'); Route::apiResource('entitlements', API\V4\EntitlementsController::class); Route::apiResource('packages', API\V4\PackagesController::class); Route::apiResource('skus', API\V4\SkusController::class); Route::apiResource('users', API\V4\UsersController::class); Route::get('users/{id}/status', 'API\V4\UsersController@status'); Route::apiResource('wallets', API\V4\WalletsController::class); Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions'); Route::get('wallets/{id}/receipts', 'API\V4\WalletsController@receipts'); Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload'); Route::post('payments', 'API\V4\PaymentsController@store'); Route::get('payments/mandate', 'API\V4\PaymentsController@mandate'); Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate'); Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate'); Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete'); } ); Route::group( [ 'domain' => \config('app.domain'), 'middleware' => 'api', 'prefix' => 'v4' ], function () { Route::get('meet/openvidu/{id}', 'API\V4\OpenViduController@joinOrCreate'); } ); +Route::group( + [ + 'domain' => \config('app.domain'), + 'middleware' => 'api', + 'prefix' => 'v4' + ], + function () { + Route::get('meet/openvidu/{id}', 'API\V4\OpenViduController@joinOrCreate'); + } +); + Route::group( [ 'domain' => \config('app.domain'), ], function () { Route::post('webhooks/payment/{provider}', 'API\V4\PaymentsController@webhook'); Route::post('webhooks/meet/openvidu', 'API\V4\OpenViduController@webhook'); } ); Route::group( [ 'domain' => 'admin.' . \config('app.domain'), 'middleware' => ['auth:api', 'admin'], 'prefix' => 'v4', ], function () { Route::apiResource('domains', API\V4\Admin\DomainsController::class); Route::get('domains/{id}/confirm', 'API\V4\Admin\DomainsController@confirm'); Route::apiResource('entitlements', API\V4\Admin\EntitlementsController::class); Route::apiResource('packages', API\V4\Admin\PackagesController::class); Route::apiResource('skus', API\V4\Admin\SkusController::class); Route::apiResource('users', API\V4\Admin\UsersController::class); Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend'); Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend'); Route::apiResource('wallets', API\V4\Admin\WalletsController::class); Route::post('wallets/{id}/one-off', 'API\V4\Admin\WalletsController@oneOff'); Route::get('wallets/{id}/transactions', 'API\V4\Admin\WalletsController@transactions'); Route::apiResource('discounts', API\V4\Admin\DiscountsController::class); } );