Complete the release radar to notify new 2Fauth releases - Close #127

This commit is contained in:
Bubka 2022-10-12 11:10:51 +02:00
parent e99c684018
commit 8d3a97a701
15 changed files with 163 additions and 108 deletions

View File

@ -5,24 +5,19 @@ namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ReleaseRadarActivated class ScanForNewReleaseCalled
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* @var \App\Models\Group
*/
// public $group;
/** /**
* Create a new event instance. * Create a new event instance.
* *
* @param \App\Models\Group $group
* @return void * @return void
*/ */
public function __construct() public function __construct()
{ {
// $this->group = $group; Log::info('ReleaseRadarActivated event dispatched');
} }
} }

View File

@ -17,4 +17,10 @@ class Helpers
{ {
return Str::random(40).'.'.$extension; return Str::random(40).'.'.$extension;
} }
public static function cleanVersionNumber(?string $release): string|false
{
return preg_match('/([[0-9][0-9\.]*[0-9])/', $release, $version) ? $version[0] : false;
}
} }

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Facades\Settings; use App\Facades\Settings;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use App\Events\ScanForNewReleaseCalled;
class SinglePageController extends Controller class SinglePageController extends Controller
{ {
@ -15,6 +16,8 @@ class SinglePageController extends Controller
*/ */
public function index() public function index()
{ {
event(new ScanForNewReleaseCalled());
return view('landing')->with([ return view('landing')->with([
'appSettings' => Settings::all()->toJson(), 'appSettings' => Settings::all()->toJson(),
'appConfig' => collect([ 'appConfig' => collect([

View File

@ -2,9 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\ReleaseRadarActivated;
use App\Services\ReleaseRadarService; use App\Services\ReleaseRadarService;
use Illuminate\Support\Facades\App;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Facades\Settings; use App\Facades\Settings;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -20,23 +18,23 @@ class SystemController extends Controller
public function infos(Request $request) public function infos(Request $request)
{ {
$infos = array(); $infos = array();
$infos['Date'] = date(DATE_RFC2822); $infos['Date'] = date(DATE_RFC2822);
$infos['userAgent'] = $request->header('user-agent'); $infos['userAgent'] = $request->header('user-agent');
// App info // App info
$infos['Version'] = config('2fauth.version'); $infos['Version'] = config('2fauth.version');
$infos['Environment'] = config('app.env'); $infos['Environment'] = config('app.env');
$infos['Debug'] = var_export(config('app.debug'), true); $infos['Debug'] = var_export(config('app.debug'), true);
$infos['Cache driver'] = config('cache.default'); $infos['Cache driver'] = config('cache.default');
$infos['Log channel'] = config('logging.default'); $infos['Log channel'] = config('logging.default');
$infos['Log level'] = env('LOG_LEVEL'); $infos['Log level'] = env('LOG_LEVEL');
$infos['DB driver'] = DB::getDriverName(); $infos['DB driver'] = DB::getDriverName();
// PHP info // PHP info
$infos['PHP version'] = PHP_VERSION; $infos['PHP version'] = PHP_VERSION;
$infos['Operating system'] = PHP_OS; $infos['Operating system'] = PHP_OS;
$infos['interface'] = PHP_SAPI; $infos['interface'] = PHP_SAPI;
// Auth info // Auth info
if ($request->user()) { if ($request->user()) {
$infos['Auth guard'] = config('auth.defaults.guard'); $infos['Auth guard'] = config('auth.defaults.guard');
if ($infos['Auth guard'] === 'reverse-proxy-guard') { if ($infos['Auth guard'] === 'reverse-proxy-guard') {
$infos['Auth proxy header for user'] = config('auth.auth_proxy_headers.user'); $infos['Auth proxy header for user'] = config('auth.auth_proxy_headers.user');
$infos['Auth proxy header for email'] = config('auth.auth_proxy_headers.email'); $infos['Auth proxy header for email'] = config('auth.auth_proxy_headers.email');
@ -46,7 +44,7 @@ class SystemController extends Controller
} }
// User info // User info
if ($request->user()) { if ($request->user()) {
$infos['options'] = Settings::all()->toArray(); $infos['options'] = Settings::all()->toArray();
} }
return response()->json($infos); return response()->json($infos);
@ -58,11 +56,10 @@ class SystemController extends Controller
* *
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function latestRelease(Request $request) public function latestRelease(Request $request, ReleaseRadarService $releaseRadar)
{ {
$releaseRadarService = App::make(ReleaseRadarService::class); $release = $releaseRadar->manualScan();
$release = $releaseRadarService->scanForRelease();
return response()->json($release); return response()->json(['newRelease' => $release]);
} }
} }

View File

@ -2,34 +2,41 @@
namespace App\Listeners; namespace App\Listeners;
use App\Events\ReleaseRadarActivated; use App\Events\ScanForNewReleaseCalled;
use App\Services\ReleaseRadarService; use App\Services\ReleaseRadarService;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class ReleaseRadar class ReleaseRadar
{ {
/**
* @var ReleaseRadarService $releaseRadar
*/
protected $releaseRadar;
/** /**
* Create the event listener. * Create the event listener.
*
* @param \App\Services\ReleaseRadarService $releaseRadar
* *
* @return void * @return void
*/ */
public function __construct() public function __construct(ReleaseRadarService $releaseRadar)
{ {
// $this->releaseRadar = $releaseRadar;
} }
/** /**
* Handle the event. * Handle the event.
* *
* @param \App\Events\ReleaseRadarActivated $event * @param \App\Events\ScanForNewReleaseCalled $event
* @return void * @return void
*/ */
public function handle(ReleaseRadarActivated $event) public function handle(ScanForNewReleaseCalled $event)
{ {
Log::info('Release radar activated'); $this->releaseRadar->scheduledScan();
Log::info('Scheduled release scan complete');
$releaseRadarService = App::make(ReleaseRadarService::class);
$releaseRadarService->scanForNewRelease();
} }
} }

View File

@ -4,7 +4,7 @@ namespace App\Providers;
use App\Events\GroupDeleting; use App\Events\GroupDeleting;
use App\Events\TwoFAccountDeleted; use App\Events\TwoFAccountDeleted;
use App\Events\ReleaseRadarActivated; use App\Events\ScanForNewReleaseCalled;
use App\Listeners\ReleaseRadar; use App\Listeners\ReleaseRadar;
use App\Listeners\CleanIconStorage; use App\Listeners\CleanIconStorage;
use App\Listeners\DissociateTwofaccountFromGroup; use App\Listeners\DissociateTwofaccountFromGroup;
@ -29,7 +29,7 @@ class EventServiceProvider extends ServiceProvider
GroupDeleting::class => [ GroupDeleting::class => [
DissociateTwofaccountFromGroup::class, DissociateTwofaccountFromGroup::class,
], ],
ReleaseRadarActivated::class => [ ScanForNewReleaseCalled::class => [
ReleaseRadar::class, ReleaseRadar::class,
], ],
]; ];

View File

@ -56,7 +56,7 @@ class TwoFAuthServiceProvider extends ServiceProvider implements DeferrableProvi
{ {
return [ return [
LogoService::class, LogoService::class,
LogoSeReleaseRadarServicervice::class, ReleaseRadarService::class,
]; ];
} }
} }

View File

@ -3,33 +3,60 @@
namespace App\Services; namespace App\Services;
use App\Facades\Settings; use App\Facades\Settings;
use App\Helpers\Helpers;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
class ReleaseRadarService class ReleaseRadarService
{ {
/** /**
* Run a scheduled release scan
* *
* @return void
*/ */
public function scanForNewRelease() : void public function scheduledScan() : void
{ {
// Only if the last check is old enough if ((Settings::get('lastRadarScan') + 604800) < time()) {
// if ((Settings::get('lastRadarScan') + 604800) < time()) { $this->newRelease();
if ($latestReleaseData = json_decode($this->GetLatestReleaseData())) }
{ }
if ($latestReleaseData->prerelease == false && $latestReleaseData->draft == false) {
Settings::set('lastRadarScan', time());
Settings::set('lastRadarScan', time()); /**
} * Run a manual release scan
*
* @return false|string False if no new release, the new release number otherwise
*/
public function manualScan() : false|string
{
return $this->newRelease();
}
/**
* Run a release scan
*
* @return false|string False if no new release, the new release number otherwise
*/
protected function newRelease() : false|string
{
if ($latestReleaseData = json_decode($this->getLatestReleaseData()))
{
$githubVersion = Helpers::cleanVersionNumber($latestReleaseData->tag_name);
$installedVersion = Helpers::cleanVersionNumber(config('2fauth.version'));
if ($githubVersion > $installedVersion && $latestReleaseData->prerelease == false && $latestReleaseData->draft == false) {
Settings::set('latestRelease', $latestReleaseData->tag_name);
return $latestReleaseData->tag_name;
}
else {
Settings::delete('latestRelease');
} }
return $latestReleaseData; Settings::set('lastRadarScan', time());
// } }
// tag_name return false;
// prerelease
// draft
} }
@ -38,9 +65,8 @@ class ReleaseRadarService
* *
* @return string|null * @return string|null
*/ */
protected function GetLatestReleaseData() : string|null protected function getLatestReleaseData() : string|null
{ {
return null;
try { try {
$response = Http::retry(3, 100) $response = Http::retry(3, 100)
->get(config('2fauth.latestReleaseUrl')); ->get(config('2fauth.latestReleaseUrl'));

View File

@ -67,7 +67,7 @@ return [
'getOfficialIcons' => true, 'getOfficialIcons' => true,
'checkForUpdate' => true, 'checkForUpdate' => true,
'lastRadarScan' => 0, 'lastRadarScan' => 0,
'latestRelease' => '', 'latestRelease' => false,
], ],
]; ];

View File

@ -13,7 +13,9 @@
</router-link> </router-link>
</div> </div>
<div v-else class="content has-text-centered"> <div v-else class="content has-text-centered">
<router-link id="lnkSettings" :to="{ name: 'settings.options' }" class="has-text-grey">{{ $t('settings.settings') }}</router-link> <router-link id="lnkSettings" :to="{ name: 'settings.options' }" class="has-text-grey">
{{ $t('settings.settings') }}<span v-if="$root.appSettings.latestRelease && $root.appSettings.checkForUpdate" class="release-flag"></span>
</router-link>
<span v-if="!this.$root.appConfig.proxyAuth || (this.$root.appConfig.proxyAuth && this.$root.appConfig.proxyLogoutUrl)"> <span v-if="!this.$root.appConfig.proxyAuth || (this.$root.appConfig.proxyAuth && this.$root.appConfig.proxyLogoutUrl)">
- <button id="lnkSignOut" class="button is-text is-like-text has-text-grey" @click="logout">{{ $t('auth.sign_out') }}</button> - <button id="lnkSignOut" class="button is-text is-like-text has-text-grey" @click="logout">{{ $t('auth.sign_out') }}</button>
</span> </span>

View File

@ -0,0 +1,42 @@
<template>
<div class="columns is-mobile is-vcentered">
<div class="column is-narrow">
<button type="button" :class="isScanning ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="getLatestRelease">Check now</button>
</div>
<div class="column">
<span v-if="$root.appSettings.latestRelease" class="mt-2 has-text-warning">
<span class="release-flag"></span>{{ $root.appSettings.latestRelease }} is available <a class="is-size-7" href="https://github.com/Bubka/2FAuth/releases">View on Github</a>
</span>
<span v-if="isUpToDate" class="has-text-grey">
{{ $t('commons.you_are_up_to_date') }}
</span>
</div>
</div>
</template>
<script>
export default {
name: 'VersionChecker',
data() {
return {
isScanning: false,
isUpToDate: null,
}
},
methods: {
async getLatestRelease() {
this.isScanning = true;
await this.axios.get('/latestRelease').then(response => {
this.$root.appSettings['latestRelease'] = response.data.newRelease
this.isUpToDate = response.data.newRelease === false
})
this.isScanning = false;
},
}
}
</script>

View File

@ -7,22 +7,9 @@
{{ $t('commons.2fauth_teaser')}} {{ $t('commons.2fauth_teaser')}}
</p> </p>
<img src="logo.svg" style="height: 32px" alt="2FAuth logo" /> <img src="logo.svg" style="height: 32px" alt="2FAuth logo" />
<p class="block mb-6"> <p class="block" :class="showUserOptions ? 'mb-5' : '' ">
©Bubka <a class="is-size-7" href="https://github.com/Bubka/2FAuth/blob/master/LICENSE">AGPL-3.0 license</a> ©Bubka <a class="is-size-7" href="https://github.com/Bubka/2FAuth/blob/master/LICENSE">AGPL-3.0 license</a>
</p> </p>
<div v-if="showUserOptions" class="block">
<form>
<form-checkbox
v-on:checkForUpdate="saveSetting('checkForUpdate', $event)"
:form="form"
fieldName="checkForUpdate"
:label="$t('commons.check_for_update')"
:help="$t('commons.check_for_update_help')"
labelClass="has-text-weight-normal"
/>
</form>
<button type="button" class="button is-link" @click="getLatestRelease">{{ $t('commons.cancel') }}</button>
</div>
<h2 class="title is-5 has-text-grey-light"> <h2 class="title is-5 has-text-grey-light">
{{ $t('commons.resources') }} {{ $t('commons.resources') }}
</h2> </h2>
@ -66,7 +53,7 @@
{{ $t('commons.environment') }} {{ $t('commons.environment') }}
</h2> </h2>
<div class="box has-background-black-bis is-family-monospace is-size-7"> <div class="box has-background-black-bis is-family-monospace is-size-7">
<button class="button copy-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listInfos.innerText" v-clipboard:success="clipboardSuccessHandler"> <button class="button is-like-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listInfos.innerText" v-clipboard:success="clipboardSuccessHandler">
<font-awesome-icon :icon="['fas', 'copy']" /> <font-awesome-icon :icon="['fas', 'copy']" />
</button> </button>
<ul ref="listInfos"> <ul ref="listInfos">
@ -78,7 +65,7 @@
{{ $t('settings.user_options') }} {{ $t('settings.user_options') }}
</h2> </h2>
<div class="box has-background-black-bis is-family-monospace is-size-7"> <div class="box has-background-black-bis is-family-monospace is-size-7">
<button class="button copy-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listUserOptions.innerText" v-clipboard:success="clipboardSuccessHandler"> <button class="button is-like-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listUserOptions.innerText" v-clipboard:success="clipboardSuccessHandler">
<font-awesome-icon :icon="['fas', 'copy']" /> <font-awesome-icon :icon="['fas', 'copy']" />
</button> </button>
<ul ref="listUserOptions"> <ul ref="listUserOptions">
@ -98,17 +85,12 @@
</template> </template>
<script> <script>
import Form from './../components/Form'
export default { export default {
data() { data() {
return { return {
infos : null, infos : null,
options : null, options : null,
showUserOptions: false, showUserOptions: false,
form: new Form({
checkForUpdate: null,
}),
} }
}, },
@ -122,34 +104,10 @@
this.showUserOptions = true this.showUserOptions = true
} }
}) })
await this.form.get('/api/v1/settings/checkForUpdate', {returnError: true}).then(response => {
if (response.status === 200) {
this.form.fillWithKeyValueObject(response.data)
}
})
.catch(error => {
// do nothing
})
}, },
methods: { methods: {
saveSetting(settingName, event) {
this.axios.put('/api/v1/settings/' + settingName, { value: event }).then(response => {
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
this.$root.appSettings[response.data.key] = response.data.value
})
},
async getLatestRelease() {
await this.axios.get('latestRelease').then(response => {
console.log(response.data)
})
},
clipboardSuccessHandler ({ value, event }) { clipboardSuccessHandler ({ value, event }) {
this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') }) this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') })
}, },

View File

@ -6,6 +6,9 @@
<!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> --> <!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> -->
<form> <form>
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4> <h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
<!-- Check for update -->
<form-checkbox v-on:checkForUpdate="saveSetting('checkForUpdate', $event)" :form="form" fieldName="checkForUpdate" :label="$t('commons.check_for_update')" :help="$t('commons.check_for_update_help')" />
<version-checker></version-checker>
<!-- Language --> <!-- Language -->
<form-select v-on:lang="saveSetting('lang', $event)" :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" /> <form-select v-on:lang="saveSetting('lang', $event)" :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
<div class="field help"> <div class="field help">
@ -22,7 +25,6 @@
<!-- Official icons --> <!-- Official icons -->
<form-checkbox v-on:getOfficialIcons="saveSetting('getOfficialIcons', $event)" :form="form" fieldName="getOfficialIcons" :label="$t('settings.forms.get_official_icons.label')" :help="$t('settings.forms.get_official_icons.help')" /> <form-checkbox v-on:getOfficialIcons="saveSetting('getOfficialIcons', $event)" :form="form" fieldName="getOfficialIcons" :label="$t('settings.forms.get_official_icons.label')" :help="$t('settings.forms.get_official_icons.help')" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4> <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
<!-- default group --> <!-- default group -->
<form-select v-on:defaultGroup="saveSetting('defaultGroup', $event)" :options="groups" :form="form" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" /> <form-select v-on:defaultGroup="saveSetting('defaultGroup', $event)" :options="groups" :form="form" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" />
@ -41,7 +43,6 @@
<!-- copy otp on get --> <!-- copy otp on get -->
<form-checkbox v-on:copyOtpOnDisplay="saveSetting('copyOtpOnDisplay', $event)" :form="form" fieldName="copyOtpOnDisplay" :label="$t('settings.forms.copy_otp_on_display.label')" :help="$t('settings.forms.copy_otp_on_display.help')" /> <form-checkbox v-on:copyOtpOnDisplay="saveSetting('copyOtpOnDisplay', $event)" :form="form" fieldName="copyOtpOnDisplay" :label="$t('settings.forms.copy_otp_on_display.label')" :help="$t('settings.forms.copy_otp_on_display.help')" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4> <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
<!-- basic qrcode --> <!-- basic qrcode -->
<form-checkbox v-on:useBasicQrcodeReader="saveSetting('useBasicQrcodeReader', $event)" :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" /> <form-checkbox v-on:useBasicQrcodeReader="saveSetting('useBasicQrcodeReader', $event)" :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
@ -82,6 +83,7 @@
*/ */
import Form from './../../components/Form' import Form from './../../components/Form'
import VersionChecker from './../../components/VersionChecker'
export default { export default {
data(){ data(){
@ -101,6 +103,7 @@
defaultCaptureMode: '', defaultCaptureMode: '',
rememberActiveGroup: true, rememberActiveGroup: true,
getOfficialIcons: null, getOfficialIcons: null,
checkForUpdate: null,
}), }),
layouts: [ layouts: [
{ text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' }, { text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' },
@ -129,6 +132,10 @@
} }
}, },
components: {
VersionChecker,
},
computed : { computed : {
langs: function() { langs: function() {
let locales = [{ let locales = [{

View File

@ -60,8 +60,9 @@ return [
'logos_by' => 'Logos by', 'logos_by' => 'Logos by',
'search' => 'Search', 'search' => 'Search',
'resources' => 'Resources', 'resources' => 'Resources',
'check_for_update' => 'Check regularly for new version', 'check_for_update' => 'Check for new version',
'check_for_update_help' => 'Automatically check (once a week) and warn when a new release of 2FAuth is published on Github', 'check_for_update_help' => 'Automatically check (once a week) and warn when a new release of 2FAuth is published on Github',
'you_are_up_to_date' => 'This instance is up-to-date',
'2fauth_description' => 'A web app to manage your Two-Factor Authentication (2FA) accounts and generate their security codes', '2fauth_description' => 'A web app to manage your Two-Factor Authentication (2FA) accounts and generate their security codes',
'image_of_qrcode_to_scan' => 'Image of a QR code to scan', 'image_of_qrcode_to_scan' => 'Image of a QR code to scan',
'file' => 'File', 'file' => 'File',

View File

@ -762,6 +762,17 @@ footer .field.is-grouped {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.release-flag {
height: 0.5rem;
width: 0.5rem;
display: inline-block;
border: none;
border-radius: 50%;
background-color: $warning;
vertical-align: middle;
margin: 0 5px;
}
.fadeInOut-enter-active { .fadeInOut-enter-active {
animation: fadeIn 500ms animation: fadeIn 500ms