Page MenuHomePhorge

D3029.1775153412.diff
No OneTemporary

Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None

D3029.1775153412.diff

diff --git a/src/app/Http/Controllers/API/V4/CompanionAppsController.php b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
--- a/src/app/Http/Controllers/API/V4/CompanionAppsController.php
+++ b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
@@ -2,11 +2,14 @@
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\ResourceController;
+use App\Utils;
+use App\Tenant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
+use BaconQrCode;
-class CompanionAppsController extends Controller
+class CompanionAppsController extends ResourceController
{
/**
* Register a companion app.
@@ -55,4 +58,131 @@
return response()->json(['status' => 'success']);
}
+
+
+ /**
+ * Generate a QR-code image for a string
+ *
+ * @param string $data data to encode
+ *
+ * @return string
+ */
+ public static function generateQRCode($data)
+ {
+ $renderer_style = new BaconQrCode\Renderer\RendererStyle\RendererStyle(300, 1);
+ $renderer_image = new BaconQrCode\Renderer\Image\SvgImageBackEnd();
+ $renderer = new BaconQrCode\Renderer\ImageRenderer($renderer_style, $renderer_image);
+ $writer = new BaconQrCode\Writer($renderer);
+
+ return 'data:image/svg+xml;base64,' . base64_encode($writer->writeString($data));
+ }
+
+ /**
+ * Delete a resource.
+ *
+ * @param string $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ $result = \App\CompanionApp::find($id);
+ if (!$result) {
+ return $this->errorResponse(404);
+ }
+
+ $user = $this->guard()->user();
+ if ($user->id != $result->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ $result->delete();
+ return response()->json();
+ }
+
+ /**
+ * List devices.
+ *
+ * The user-entitlements billed to the current user wallet(s)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = $this->guard()->user();
+ $search = trim(request()->input('search'));
+ $page = intval(request()->input('page')) ?: 1;
+ $pageSize = 20;
+ $hasMore = false;
+
+ $result = \App\CompanionApp::where('user_id', $user->id);
+
+ $result = $result->orderBy('created_at')
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ // Process the result
+ $result = $result->map(
+ function ($device) {
+ return $device->toArray();
+ }
+ );
+
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ ];
+
+ return response()->json($result);
+ }
+
+ /**
+ * Get the information about the specified companion app.
+ *
+ * @param string $id CompanionApp identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ $result = \App\CompanionApp::find($id);
+ if (!$result) {
+ return $this->errorResponse(404);
+ }
+
+ $user = $this->guard()->user();
+ if ($user->id != $result->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ return response()->json($result->toArray());
+ }
+
+ /**
+ * Get the information about the specified companion app.
+ *
+ * @return \Illuminate\Http\JsonResponse|void
+ */
+ public function pairing()
+ {
+ $user = $this->guard()->user();
+
+ $response['qrcode'] = self::generateQRCode(
+ json_encode([
+ "serverUrl" => Utils::serviceUrl('', $user->tenant_id),
+ "clientIdentifier" => \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id'),
+ "clientSecret" => \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_secret'),
+ "username" => $user->email
+ ])
+ );
+
+ return response()->json($response);
+ }
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -180,6 +180,7 @@
'enableResources' => $isController && $hasCustomDomain && in_array('beta-resources', $skus),
'enableUsers' => $isController,
'enableWallets' => $isController,
+ 'enableCompanionapps' => $isController,
];
return array_merge($process, $result);
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -15,6 +15,7 @@
],
"require": {
"php": "^7.3",
+ "bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-dompdf": "^0.8.6",
"doctrine/dbal": "^2.13",
"dyrynda/laravel-nullable-fields": "*",
diff --git a/src/config/auth.php b/src/config/auth.php
--- a/src/config/auth.php
+++ b/src/config/auth.php
@@ -118,6 +118,11 @@
'client_secret' => env('PASSPORT_PROXY_OAUTH_CLIENT_SECRET'),
],
+ 'companion_app' => [
+ 'client_id' => env('PASSPORT_COMPANIONAPP_OAUTH_CLIENT_ID'),
+ 'client_secret' => env('PASSPORT_COMPANIONAPP_OAUTH_CLIENT_SECRET'),
+ ],
+
'token_expiry_minutes' => env('OAUTH_TOKEN_EXPIRY', 60),
'refresh_token_expiry_minutes' => env('OAUTH_REFRESH_TOKEN_EXPIRY', 30 * 24 * 60),
];
diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -20,7 +20,9 @@
faUniversity,
faExclamationCircle,
faInfoCircle,
+ faMinusCircle,
faLock,
+ faMobile,
faKey,
faPlus,
faSearch,
@@ -54,7 +56,9 @@
faFolderOpen,
faGlobe,
faInfoCircle,
+ faMinusCircle,
faLock,
+ faMobile,
faKey,
faPlus,
faSearch,
diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js
--- a/src/resources/js/user/routes.js
+++ b/src/resources/js/user/routes.js
@@ -8,6 +8,7 @@
// Note: you can pack multiple components into the same chunk, webpackChunkName
// is also used to get a sensible file name instead of numbers
+const CompanionAppComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/CompanionApp')
const DashboardComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Dashboard')
const DistlistInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Distlist/Info')
const DistlistListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Distlist/List')
@@ -44,6 +45,12 @@
component: DistlistListComponent,
meta: { requiresAuth: true, perm: 'distlists' }
},
+ {
+ path: '/companion',
+ name: 'companion',
+ component: CompanionAppComponent,
+ meta: { requiresAuth: true, perm: 'companionapps' }
+ },
{
path: '/domain/:domain',
name: 'domain',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -41,6 +41,7 @@
'beta' => "beta",
'distlists' => "Distribution lists",
'chat' => "Video chat",
+ 'companion' => "Companion app",
'domains' => "Domains",
'invitations' => "Invitations",
'profile' => "Your profile",
diff --git a/src/resources/vue/CompanionApp.vue b/src/resources/vue/CompanionApp.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/CompanionApp.vue
@@ -0,0 +1,81 @@
+<template>
+ <div class="container" dusk="companionapp-component">
+ <div class="card">
+ <div class="card-body">
+ <div class="card-title">Companion App</div>
+ <div class="card-text">
+ <p>
+ Use the Companion App on your mobile phone for advanced two factor authentication.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <ul class="nav nav-tabs mt-2" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="tab-qrcode" href="#companion-qrcode" role="tab" aria-controls="companion-qrcode" aria-selected="true">
+ Pair new device
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-list" href="#companion-list" role="tab" aria-controls="companion-list" aria-selected="false">
+ Paired devices
+ </a>
+ </li>
+ </ul>
+
+ <div class="tab-content">
+ <div class="tab-pane active" id="companion-qrcode" role="tabpanel" aria-labelledby="tab-qrcode">
+ <div class="card-body">
+ <div class="card-text">
+ <p>
+ Pair a new device using the following QR-Code:
+ </p>
+ <p>
+ <img :src="qrcode" />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="companion-list" role="tabpanel" aria-labelledby="tab-list">
+ <div class="card-body">
+ <companionapp-list class="card-text"></companionapp-list>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</template>
+
+<script>
+ import { Modal } from 'bootstrap'
+ import CompanionappList from './Widgets/CompanionappList'
+
+ export default {
+ components: {
+ CompanionappList
+ },
+ data() {
+ return {
+ qrcode: ""
+ }
+ },
+ updated() {
+ $(this.$el).find('ul.nav-tabs a').on('click', e => {
+ this.$root.tab(e)
+ })
+ },
+ mounted() {
+ this.$root.startLoading()
+
+ axios.get('/api/v4/companion/pairing')
+ .then(response => {
+ this.$root.stopLoading()
+ this.qrcode = response.data.qrcode
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ },
+ }
+</script>
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -32,6 +32,9 @@
<a v-if="webmailURL" class="card link-webmail" :href="webmailURL">
<svg-icon icon="envelope"></svg-icon><span class="name">{{ $t('dashboard.webmail') }}</span>
</a>
+ <router-link v-if="status.enableCompanionapps" class="card link-companionapp" :to="{ name: 'companion' }">
+ <svg-icon icon="mobile"></svg-icon><span class="name">{{ $t('dashboard.companion') }}</span>
+ </router-link>
</div>
</div>
</template>
diff --git a/src/resources/vue/Widgets/CompanionappList.vue b/src/resources/vue/Widgets/CompanionappList.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/CompanionappList.vue
@@ -0,0 +1,55 @@
+<template>
+ <div>
+ <table class="table table-sm m-0 entries">
+ <thead>
+ <tr>
+ <th scope="col">{{ $t('form.date') }}</th>
+ <th scope="col">Device ID</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="entry in entries" :id="'entry' + entry.id" :key="entry.id">
+ <td class="datetime">{{ entry.created_at }}</td>
+ <td class="description">{{ entry.device_id }}</td>
+ <td class="selection">
+ <button class="btn btn-lg btn-link btn-action" title="Forget Device" type="button"
+ @click="remove(entry.id)"
+ >
+ <svg-icon icon="minus-circle"></svg-icon>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ <list-foot text="There are currently no devices" :colspan="3"></list-foot>
+ </table>
+ <list-more v-if="hasMore" :on-click="loadMore"></list-more>
+ </div>
+</template>
+
+<script>
+ import ListTools from './ListTools'
+
+ export default {
+ mixins: [ ListTools ],
+ props: {
+ },
+ data() {
+ return {
+ entries: []
+ }
+ },
+ mounted() {
+ this.loadMore({ reset: true })
+ },
+ methods: {
+ loadMore(params) {
+ this.listSearch('entries', '/api/v4/companion/', params)
+ },
+ remove(id) {
+ axios.delete('/api/v4/companion/' + id)
+ .catch(this.$root.errorHandler)
+ },
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -61,8 +61,6 @@
'prefix' => $prefix . 'api/v4'
],
function () {
- Route::post('companion/register', 'API\V4\CompanionAppsController@register');
-
Route::post('auth-attempts/{id}/confirm', 'API\V4\AuthAttemptsController@confirm');
Route::post('auth-attempts/{id}/deny', 'API\V4\AuthAttemptsController@deny');
Route::get('auth-attempts/{id}/details', 'API\V4\AuthAttemptsController@details');
@@ -112,6 +110,10 @@
Route::get('payments/methods', 'API\V4\PaymentsController@paymentMethods');
Route::get('payments/pending', 'API\V4\PaymentsController@payments');
Route::get('payments/has-pending', 'API\V4\PaymentsController@hasPayments');
+
+ Route::get('companion/pairing', 'API\V4\CompanionAppsController@pairing');
+ Route::apiResource('companion', 'API\V4\CompanionAppsController');
+ Route::post('companion/register', 'API\V4\CompanionAppsController@register');
}
);

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 2, 6:10 PM (10 h, 8 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820212
Default Alt Text
D3029.1775153412.diff (14 KB)

Event Timeline