Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117830576
D3029.1775302464.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None
D3029.1775302464.diff
View Options
diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -161,6 +161,10 @@
#PASSPORT_PROXY_OAUTH_CLIENT_ID=
#PASSPORT_PROXY_OAUTH_CLIENT_SECRET=
+# Generate with ./artisan passport:client --password
+#PASSPORT_COMPANIONAPP_OAUTH_CLIENT_ID=
+#PASSPORT_COMPANIONAPP_OAUTH_CLIENT_SECRET=
+
PASSPORT_PRIVATE_KEY=
PASSPORT_PUBLIC_KEY=
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,138 @@
return response()->json(['status' => 'success']);
}
+
+
+ /**
+ * Generate a QR-code image for a string
+ *
+ * @param string $data data to encode
+ *
+ * @return string
+ */
+ private 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();
+
+ $clientIdentifier = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id');
+ $clientSecret = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_secret');
+ if (empty($clientIdentifier) || empty($clientSecret)) {
+ \Log::warning("Empty client identifier or secret. Can't generate qr-code.");
+ return $this->errorResponse(500);
+ }
+
+ $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
@@ -192,6 +192,7 @@
'enableSettings' => $isController,
'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/database/seeds/local/OauthClientSeeder.php b/src/database/seeds/local/OauthClientSeeder.php
--- a/src/database/seeds/local/OauthClientSeeder.php
+++ b/src/database/seeds/local/OauthClientSeeder.php
@@ -30,5 +30,20 @@
$client->id = \config('auth.proxy.client_id');
$client->save();
+
+ $companionAppClient = Passport::client()->forceFill([
+ 'user_id' => null,
+ 'name' => "CompanionApp Password Grant Client",
+ 'secret' => \config('auth.companion_app.client_secret'),
+ 'provider' => 'users',
+ 'redirect' => 'https://' . \config('app.website_domain'),
+ 'personal_access_client' => 0,
+ 'password_client' => 1,
+ 'revoked' => false,
+ ]);
+
+ $companionAppClient->id = \config('auth.companion_app.client_id');
+
+ $companionAppClient->save();
}
}
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
@@ -21,7 +21,9 @@
faUniversity,
faExclamationCircle,
faInfoCircle,
+ faMinusCircle,
faLock,
+ faMobile,
faKey,
faPlus,
faSearch,
@@ -57,7 +59,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')
@@ -45,6 +46,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
@@ -42,6 +42,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
@@ -38,6 +38,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
@@ -63,13 +63,15 @@
'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');
Route::get('auth-attempts', 'API\V4\AuthAttemptsController@index');
+ Route::apiResource('companion', 'API\V4\CompanionAppsController');
+ Route::get('companion/pairing', 'API\V4\CompanionAppsController@pairing');
+ Route::post('companion/register', 'API\V4\CompanionAppsController@register');
+
Route::apiResource('domains', 'API\V4\DomainsController');
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/skus', 'API\V4\SkusController@domainSkus');
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 11:34 AM (12 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827199
Default Alt Text
D3029.1775302464.diff (15 KB)
Attached To
Mode
D3029: CompanionApp support
Attached
Detach File
Event Timeline