Add Admin role & split settings between appSettings and userPreferences

This commit is contained in:
Bubka 2023-02-17 17:12:53 +01:00
parent d0401ced5d
commit 5e5e50d053
36 changed files with 389 additions and 215 deletions

View File

@ -98,8 +98,8 @@ public function destroy(string $settingName)
abort(404);
}
$optionsConfig = config('2fauth.options');
if (array_key_exists($settingName, $optionsConfig)) {
$appSettings = config('2fauth.settings');
if (array_key_exists($settingName, $appSettings)) {
return response()->json(
['message' => 'bad request',
'reason' => [__('errors.delete_user_setting_only')],

View File

@ -3,9 +3,11 @@
namespace App\Api\v1\Controllers;
use App\Api\v1\Resources\UserResource;
use App\Api\v1\Requests\SettingUpdateRequest;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class UserController extends Controller
{
@ -25,4 +27,67 @@ public function show(Request $request)
? new UserResource($user)
: response()->json(['name' => null], 200);
}
/**
* List all preferences
*
* @return \Illuminate\Http\JsonResponse
*/
public function allPreferences(Request $request)
{
$preferences = $request->user()->preferences;
$jsonPrefs = collect([]);
$preferences->each(function (mixed $item, string $key) use ($jsonPrefs) {
$jsonPrefs->push([
'key' => $key,
'value' => $item,
]);
});
return response()->json($jsonPrefs->all(), 200);
}
/**
* Display a preference
*
* @param \Illuminate\Http\Request $request
* @param string $preferenceName
* @return \Illuminate\Http\JsonResponse
*/
public function showPreference(Request $request, string $preferenceName)
{
if (! Arr::exists($request->user()->preferences, $preferenceName)) {
abort(404);
}
return response()->json([
'key' => $preferenceName,
'value' => $request->user()->preferences[$preferenceName],
], 200);
}
/**
* Save a preference
*
* @param \App\Api\v1\Requests\SettingUpdateRequest $request
* @param string $preferenceName
* @return \Illuminate\Http\JsonResponse
*/
public function setPreference(SettingUpdateRequest $request, string $preferenceName)
{
if (! Arr::exists($request->user()->preferences, $preferenceName)) {
abort(404);
}
$validated = $request->validated();
$request->user()['preferences->'.$preferenceName] = $validated['value'];
$request->user()->save();
return response()->json([
'key' => $preferenceName,
'value' => $request->user()->preferences[$preferenceName],
], 201);
}
}

View File

@ -20,9 +20,10 @@ class UserResource extends JsonResource
public function toArray($request)
{
return [
'id' => $this->when(! is_null($request->user()), $this->id),
'name' => $this->name,
'email' => $this->when(! is_null($request->user()), $this->email),
'id' => $this->when(! is_null($request->user()), $this->id),
'name' => $this->name,
'email' => $this->when(! is_null($request->user()), $this->email),
'is_admin' => $this->when(! is_null($request->user()), $this->is_admin),
];
}
}

View File

@ -48,6 +48,12 @@ public function register()
], 404);
});
$this->renderable(function (\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException $exception, $request) {
return response()->json([
'message' => 'unauthorized',
], 403);
});
$this->renderable(function (InvalidOtpParameterException $exception, $request) {
return response()->json([
'message' => 'invalid OTP parameters',

View File

@ -4,6 +4,7 @@
use App\Events\ScanForNewReleaseCalled;
use App\Facades\Settings;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\App;
class SinglePageController extends Controller
@ -20,18 +21,17 @@ public function index()
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';
return view('landing')->with([
'theme' => Settings::get('theme'),
'appSettings' => Settings::all()->toJson(),
'appConfig' => collect([
'proxyAuth' => config('auth.defaults.guard') === 'reverse-proxy-guard' ? true : false,
'proxyLogoutUrl' => config('2fauth.config.proxyLogoutUrl') ? config('2fauth.config.proxyLogoutUrl') : false,
'subdirectory' => $subdir,
])->toJson(),
'subdirectory' => $subdir,
'lang' => App::currentLocale(),
'isDemoApp' => config('2fauth.config.isDemoApp') ? 'true' : 'false',
'isTestingApp' => config('2fauth.config.isTestingApp') ? 'true' : 'false',
'locales' => collect(config('2fauth.locales'))->toJson(), /** @phpstan-ignore-line */
'userPreferences' => Auth::user()->preferences ?? collect(config('2fauth.preferences')),
'subdirectory' => $subdir,
'isDemoApp' => config('2fauth.config.isDemoApp') ? 'true' : 'false',
'isTestingApp' => config('2fauth.config.isTestingApp') ? 'true' : 'false',
'locales' => collect(config('2fauth.locales'))->toJson(), /** @phpstan-ignore-line */
]);
}
}

View File

@ -72,6 +72,7 @@ class Kernel extends HttpKernel
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'admin' => \App\Http\Middleware\AdminOnly::class,
'guest' => \App\Http\Middleware\RejectIfAuthenticated::class,
'SkipIfAuthenticated' => \App\Http\Middleware\SkipIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Auth\Access\AuthorizationException;
class AdminOnly
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! Auth::user()->is_admin) {
throw new AuthorizationException;
}
return $next($request);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use Illuminate\Support\Facades\App;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
@ -33,6 +34,14 @@ protected function authenticate($request, array $guards)
if ($this->auth->guard($guard)->check()) {
$this->auth->shouldUse($guard);
// We now have an authenticated user so we override the locale already set
// by the SetLanguage global middleware
$lang = $this->auth->guard()->user()->preferences['lang'] ?? null;
if ($lang && in_array($lang, config('2fauth.locales')) && !App::isLocale($lang)) {
App::setLocale($lang);
}
return;
}
}

View File

@ -34,7 +34,7 @@ public function handle($request, Closure $next, ...$guards)
$inactiveFor = $now->diffInSeconds(Carbon::parse($user->last_seen_at));
// Fetch all setting values
$kickUserAfterXSecond = intval(Settings::get('kickUserAfter')) * 60;
$kickUserAfterXSecond = intval($user->preferences['kickUserAfter']) * 60;
// If user has been inactive longer than the allowed inactivity period
if ($kickUserAfterXSecond > 0 && $inactiveFor > $kickUserAfterXSecond) {

View File

@ -2,7 +2,7 @@
namespace App\Http\Middleware;
use App\Facades\Settings;
use Illuminate\Support\Facades\Auth;
use Closure;
use Illuminate\Support\Facades\App;
@ -17,34 +17,42 @@ class SetLanguage
*/
public function handle($request, Closure $next)
{
// 3 possible cases here:
// - The user has choosen a specific language among those available in the Setting view of 2FAuth
// - The client send an accept-language header
// - No language is passed from the client
// 2 possible cases here:
// - The http client send an accept-language header
// - No language is specified
//
// We prioritize the user defined one, then the request header one, and finally the fallback one.
// FI: Settings::get() always returns a fallback value
$lang = Settings::get('lang');
// We honor the language requested in the header or we use the fallback one.
// Note that if a user is authenticated later by the auth guard, the app locale
// will be overriden if the user has set a specific language in its preferences.
if ($lang === 'browser') {
$lang = config('app.fallback_locale');
$accepted = str_replace(' ', '', $request->header('Accept-Language'));
$lang = config('app.fallback_locale');
$accepted = str_replace(' ', '', $request->header('Accept-Language'));
if ($accepted && $accepted !== '*') {
$prefLocales = array_reduce(
array_diff(explode(',', $accepted), ['*']),
function ($res, $el) {
[$l, $q] = array_merge(explode(';q=', $el), [1]);
$res[$l] = (float) $q;
if ($accepted && $accepted !== '*') {
$prefLocales = array_reduce(
array_diff(explode(',', $accepted), ['*']),
function ($langs, $langItem) {
[$langLong, $weight] = array_merge(explode(';q=', $langItem), [1]);
$langShort = substr($langLong, 0, 2);
if (array_key_exists($langShort, $langs)) {
if ($langs[$langShort] < $weight) {
$langs[$langShort] = (float) $weight;
}
}
else $langs[$langShort] = (float) $weight;
return $res;
},
[]
);
arsort($prefLocales);
return $langs;
},
[]
);
arsort($prefLocales);
// We only keep the primary language passed via the header.
$lang = array_key_first($prefLocales);
// We take the first accepted language available
foreach ($prefLocales as $locale => $weight) {
if (in_array($locale, config('2fauth.locales'))) {
$lang = $locale;
break;
}
}
}

View File

@ -10,6 +10,7 @@
use Illuminate\Support\Facades\Log;
use Laragear\WebAuthn\WebAuthnAuthentication;
use Laravel\Passport\HasApiTokens;
use Illuminate\Support\Arr;
class User extends Authenticatable implements WebAuthnAuthenticatable
{
@ -42,6 +43,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
*/
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
];
/**
@ -57,6 +59,19 @@ public function sendPasswordResetNotification($token)
Log::info('Password reset token sent');
}
/**
* Get Preferences attribute
*
* @param string $value
* @return \Illuminate\Support\Collection<array-key, mixed>
*/
public function getPreferencesAttribute($value)
{
$preferences = collect(config('2fauth.preferences'))->merge(json_decode($value)); /** @phpstan-ignore-line */
return $preferences;
}
/**
* set Email attribute
*

View File

@ -16,7 +16,7 @@
class SettingService
{
/**
* All user settings
* All settings
*
* @var Collection<string, mixed>
*/
@ -32,7 +32,7 @@ class SettingService
/**
* Name of the cache item where options are persisted
*/
public const CACHE_ITEM_NAME = 'userOptions';
public const CACHE_ITEM_NAME = 'adminOptions';
/**
* Constructor
@ -106,12 +106,12 @@ public function delete(string $name) : void
}
/**
* Determine if the given setting has been customized by the user
* Determine if the given setting has been edited
*
* @param string $key
* @return bool
*/
public function isUserDefined($key) : bool
public function isEdited($key) : bool
{
return DB::table('options')->where('key', $key)->exists();
}
@ -123,17 +123,14 @@ public function isUserDefined($key) : bool
*/
private function build()
{
// Get a collection of user saved options
$userOptions = DB::table('options')->pluck('value', 'key');
$userOptions->transform(function ($item, $key) {
// Get a collection of saved options
$options = DB::table('options')->pluck('value', 'key');
$options->transform(function ($item, $key) {
return $this->restoreType($item);
});
// Merge 2fauth/app config values as fallback values
$settings = collect(config('2fauth.options'))->merge($userOptions); /** @phpstan-ignore-line */
if (! Arr::has($settings, 'lang')) {
$settings['lang'] = 'browser';
}
// Merge customized values with app default values
$settings = collect(config('2fauth.settings'))->merge($options); /** @phpstan-ignore-line */
$this->settings = $settings;
}

View File

@ -46,34 +46,47 @@
/*
|--------------------------------------------------------------------------
| Application fallback for user options
| Default values for app (global) settings
| These settings can be overloaded and persisted using the SettingService
|--------------------------------------------------------------------------
|
*/
'options' => [
'settings' => [
'useEncryption' => false,
'checkForUpdate' => true,
'lastRadarScan' => 0,
'latestRelease' => false,
],
/*
|--------------------------------------------------------------------------
| Default values for user preferences
| These settings can be overloaded and persisted by each user
|--------------------------------------------------------------------------
|
*/
'preferences' => [
'showTokenAsDot' => false,
'closeOtpOnCopy' => false,
'copyOtpOnDisplay' => false,
'useBasicQrcodeReader' => false,
'displayMode' => 'list',
'showAccountsIcons' => true,
'kickUserAfter' => '15',
'kickUserAfter' => 15,
'activeGroup' => 0,
'rememberActiveGroup' => true,
'defaultGroup' => 0,
'useEncryption' => false,
'defaultCaptureMode' => 'livescan',
'useDirectCapture' => false,
'useWebauthnAsDefault' => false,
'useWebauthnOnly' => false,
'getOfficialIcons' => true,
'checkForUpdate' => true,
'lastRadarScan' => 0,
'latestRelease' => false,
'theme' => 'dark',
'theme' => 'system',
'formatPassword' => true,
'formatPasswordBy' => 0.5,
'lang' => 'browser',
],
];

View File

@ -20,6 +20,7 @@ public function run()
'name' => 'demo',
'email' => 'demo@2fauth.app',
'password' => bcrypt('demo'),
'is_admin' => 1,
]);
$groupSocialNetwork = Group::create([

View File

@ -20,6 +20,7 @@ public function run()
'name' => 'Tester',
'email' => 'testing@2fauth.app',
'password' => bcrypt('password'),
'is_admin' => 1,
]);
$groupSocialNetwork = Group::create([

View File

@ -18,6 +18,7 @@ public function run()
'name' => 'admin',
'email' => 'admin@example.org',
'password' => bcrypt('password'),
'is_admin' => 1,
]);
}
}

5
resources/js/app.js vendored
View File

@ -17,6 +17,7 @@ const app = new Vue({
data: {
appSettings: window.appSettings,
appConfig: window.appConfig,
userPreferences: window.userPreferences,
isDemoApp: window.isDemoApp,
isTestingApp: window.isTestingApp,
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)').matches
@ -24,8 +25,8 @@ const app = new Vue({
computed: {
showDarkMode: function() {
return this.appSettings.theme == 'dark' ||
(this.appSettings.theme == 'system' && this.prefersDarkScheme)
return this.userPreferences.theme == 'dark' ||
(this.userPreferences.theme == 'system' && this.prefersDarkScheme)
}
},

View File

@ -26,7 +26,7 @@
computed: {
kickInactiveUser: function () {
return parseInt(this.$root.appSettings.kickUserAfter) > 0 && this.$route.meta.requiresAuth
return parseInt(this.$root.userPreferences.kickUserAfter) > 0 && this.$route.meta.requiresAuth
}
}

View File

@ -36,7 +36,7 @@
setTimer: function() {
this.logoutTimer = setTimeout(this.logoutUser, this.$root.appSettings.kickUserAfter * 60 * 1000)
this.logoutTimer = setTimeout(this.logoutUser, this.$root.userPreferences.kickUserAfter * 60 * 1000)
},
logoutUser: function() {

View File

@ -63,14 +63,14 @@
displayedOtp() {
let pwd = this.internal_password
if (this.$root.appSettings.formatPassword && pwd.length > 0) {
const x = Math.ceil(this.$root.appSettings.formatPasswordBy < 1 ? pwd.length * this.$root.appSettings.formatPasswordBy : this.$root.appSettings.formatPasswordBy)
if (this.$root.userPreferences.formatPassword && pwd.length > 0) {
const x = Math.ceil(this.$root.userPreferences.formatPasswordBy < 1 ? pwd.length * this.$root.userPreferences.formatPasswordBy : this.$root.userPreferences.formatPasswordBy)
const chunks = pwd.match(new RegExp(`.{1,${x}}`, 'g'));
if (chunks) {
pwd = chunks.join(' ')
}
}
return this.$root.appSettings.showOtpAsDot ? pwd.replace(/[0-9]/g, '●') : pwd
return this.$root.userPreferences.showOtpAsDot ? pwd.replace(/[0-9]/g, '●') : pwd
},
},
@ -94,10 +94,10 @@
const success = this.$clipboard(rawOTP)
if (success == true) {
if(this.$root.appSettings.kickUserAfter == -1) {
if(this.$root.userPreferences.kickUserAfter == -1) {
this.appLogout()
}
else if(this.$root.appSettings.closeOtpOnCopy) {
else if(this.$root.userPreferences.closeOtpOnCopy) {
this.$parent.isActive = false
this.clearOTP()
}
@ -214,7 +214,7 @@
}
await this.axios(request).then(response => {
if(this.$root.appSettings.copyOtpOnDisplay) {
if(this.$root.userPreferences.copyOtpOnDisplay) {
this.copyOTP(response.data.password)
}
password = response.data

View File

@ -130,8 +130,8 @@
loadingLabel: 'refreshing'
}" > -->
<draggable v-model="filteredAccounts" @start="drag = true" @end="saveOrder" ghost-class="ghost" handle=".tfa-dots" animation="200" class="accounts">
<transition-group class="columns is-multiline" :class="{ 'is-centered': $root.appSettings.displayMode === 'grid' }" type="transition" :name="!drag ? 'flip-list' : null">
<div :class="[$root.appSettings.displayMode === 'grid' ? 'tfa-grid' : 'tfa-list']" class="column is-narrow" v-for="account in filteredAccounts" :key="account.id">
<transition-group class="columns is-multiline" :class="{ 'is-centered': $root.userPreferences.displayMode === 'grid' }" type="transition" :name="!drag ? 'flip-list' : null">
<div :class="[$root.userPreferences.displayMode === 'grid' ? 'tfa-grid' : 'tfa-list']" class="column is-narrow" v-for="account in filteredAccounts" :key="account.id">
<div class="tfa-container">
<transition name="slideCheckbox">
<div class="tfa-cell tfa-checkbox" v-if="editMode">
@ -143,7 +143,7 @@
</transition>
<div tabindex="0" class="tfa-cell tfa-content is-size-3 is-size-4-mobile" @click="showAccount(account)" @keyup.enter="showAccount(account)" role="button">
<div class="tfa-text has-ellipsis">
<img :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" v-if="account.icon && $root.appSettings.showAccountsIcons" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
<img :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" v-if="account.icon && $root.userPreferences.showAccountsIcons" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
{{ displayService(account.service) }}<font-awesome-icon class="has-text-danger is-size-5 ml-2" v-if="$root.appSettings.useEncryption && account.account === $t('errors.indecipherable')" :icon="['fas', 'exclamation-circle']" />
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
</div>
@ -245,8 +245,8 @@
* ~ The Edit mode
* - User are automatically pushed to the start view if there is no account to list.
* - The view is affected by :
* ~ 'appSettings.showAccountsIcons' toggle the icon visibility
* ~ 'appSettings.displayMode' change the account appearance
* ~ 'userPreferences.showAccountsIcons' toggle the icon visibility
* ~ 'userPreferences.displayMode' change the account appearance
*
* Input :
* - The 'initialEditMode' props : allows to load the view directly in Edit mode
@ -274,7 +274,7 @@
showGroupSelector: false,
moveAccountsTo: false,
form: new Form({
value: this.$root.appSettings.activeGroup,
value: this.$root.userPreferences.activeGroup,
}),
}
},
@ -288,10 +288,10 @@
return this.accounts.filter(
item => {
if( parseInt(this.$root.appSettings.activeGroup) > 0 ) {
if( parseInt(this.$root.userPreferences.activeGroup) > 0 ) {
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
item.account.toLowerCase().includes(this.search.toLowerCase())) &&
(item.group_id == parseInt(this.$root.appSettings.activeGroup))
(item.group_id == parseInt(this.$root.userPreferences.activeGroup))
}
else {
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
@ -316,7 +316,7 @@
* Returns the name of a group
*/
activeGroupName() {
let g = this.groups.find(el => el.id === parseInt(this.$root.appSettings.activeGroup))
let g = this.groups.find(el => el.id === parseInt(this.$root.userPreferences.activeGroup))
if(g) {
return g.name
@ -369,10 +369,10 @@
* Route user to the appropriate submitting view
*/
start() {
if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'advancedForm' ) {
if( this.$root.userPreferences.useDirectCapture && this.$root.userPreferences.defaultCaptureMode === 'advancedForm' ) {
this.$router.push({ name: 'createAccount' })
}
else if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'livescan' ) {
else if( this.$root.userPreferences.useDirectCapture && this.$root.userPreferences.defaultCaptureMode === 'livescan' ) {
this.$router.push({ name: 'capture' })
}
else {
@ -541,10 +541,10 @@
setActiveGroup(id) {
// In memomry saving
this.form.value = this.$root.appSettings.activeGroup = id
this.form.value = this.$root.userPreferences.activeGroup = id
// In db saving if the user set 2FAuth to memorize the active group
if( this.$root.appSettings.rememberActiveGroup ) {
if( this.$root.userPreferences.rememberActiveGroup ) {
this.form.put('/api/v1/settings/activeGroup', {returnError: true})
.then(response => {
// everything's fine

View File

@ -59,6 +59,12 @@
'originalMessage' : this.$t('errors.auth_proxy_failed_legend')
}
}
else if (this.err.status === 403) {
return {
'message' : this.$t('errors.unauthorized'),
'originalMessage' : this.$t('errors.unauthorized_legend')
}
}
else if(this.err.data) {
return this.err.data
}

View File

@ -107,8 +107,8 @@
// Reset persisted group filter to 'All' (groupId=0)
// (backend will save to change automatically)
if( parseInt(this.$root.appSettings.activeGroup) === id ) {
this.$root.appSettings.activeGroup = 0
if( parseInt(this.$root.userPreferences.activeGroup) === id ) {
this.$root.userPreferences.activeGroup = 0
}
}
}

View File

@ -11,7 +11,7 @@
<div class="column is-full quick-uploader-button" >
<div class="quick-uploader-centerer">
<!-- upload a qr code (with basic file field and backend decoding) -->
<label role="button" tabindex="0" v-if="$root.appSettings.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-main" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
<label role="button" tabindex="0" v-if="$root.userPreferences.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-main" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
{{ $t('twofaccounts.forms.upload_qrcode') }}
</label>
@ -25,7 +25,7 @@
<div class="column is-full">
<div class="block" :class="$root.showDarkMode ? 'has-text-light':'has-text-grey-dark'">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
<!-- upload a qr code -->
<div class="block has-text-link" v-if="!$root.appSettings.useBasicQrcodeReader">
<div class="block has-text-link" v-if="!$root.userPreferences.useBasicQrcodeReader">
<label role="button" tabindex="0" class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
{{ $t('twofaccounts.forms.upload_qrcode') }}
@ -109,7 +109,7 @@
created() {
this.$nextTick(() => {
if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'upload' ) {
if( this.$root.userPreferences.useDirectCapture && this.$root.userPreferences.defaultCaptureMode === 'upload' ) {
this.$refs.qrcodeInputLabel.click()
}
})

View File

@ -10,7 +10,7 @@
</div>
<div class="nav-links">
<p>{{ $t('auth.webauthn.lost_your_device') }}&nbsp;<router-link id="lnkRecoverAccount" :to="{ name: 'webauthn.lost' }" class="is-link">{{ $t('auth.webauthn.recover_your_account') }}</router-link></p>
<p v-if="!this.$root.appSettings.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;
<p v-if="!this.$root.userPreferences.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;
<a id="lnkSignWithLegacy" role="button" class="is-link" @keyup.enter="showWebauthn = false" @click="showWebauthn = false" tabindex="0">{{ $t('auth.login_and_password') }}</a>
</p>
</div>
@ -57,7 +57,7 @@
password: ''
}),
isBusy: false,
showWebauthn: this.$root.appSettings.useWebauthnAsDefault || this.$root.appSettings.useWebauthnOnly,
showWebauthn: this.$root.userPreferences.useWebauthnAsDefault || this.$root.userPreferences.useWebauthnOnly,
csrfRefresher: null,
webauthn: new WebAuthn()
}

View File

@ -3,6 +3,9 @@
<setting-tabs :activeTab="'settings.account'"></setting-tabs>
<div class="options-tabs">
<form-wrapper>
<div v-if="isAdmin" class="notification is-warning">
{{ $t('settings.you_are_administrator') }}
</div>
<form @submit.prevent="submitProfile" @keydown="formProfile.onKeydown($event)">
<div v-if="isRemoteUser" class="notification is-warning has-text-centered" v-html="$t('auth.user_account_controlled_by_proxy')" />
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4>
@ -66,12 +69,14 @@
password : '',
}),
isRemoteUser: false,
isAdmin: false,
}
},
async mounted() {
const { data } = await this.formProfile.get('/api/v1/user')
if( data.is_admin === true ) this.isAdmin = true
if( data.id === null ) this.isRemoteUser = true
this.formProfile.fill(data)

View File

@ -3,58 +3,65 @@
<setting-tabs :activeTab="'settings.options'"></setting-tabs>
<div class="options-tabs">
<form-wrapper>
<!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> -->
<form>
<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 -->
<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">
{{ $t('settings.forms.some_translation_are_missing') }}
<a class="ml-2" href="https://crowdin.com/project/2fauth">
{{ $t('settings.forms.help_translate_2fauth') }}
<font-awesome-icon :icon="['fas', 'external-link-alt']" />
</a>
<!-- user preferences -->
<div class="block">
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
<!-- Language -->
<form-select v-on:lang="savePreference('lang', $event)" :options="langs" :form="preferencesForm" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
<div class="field help">
{{ $t('settings.forms.some_translation_are_missing') }}
<a class="ml-2" href="https://crowdin.com/project/2fauth">
{{ $t('settings.forms.help_translate_2fauth') }}
<font-awesome-icon :icon="['fas', 'external-link-alt']" />
</a>
</div>
<!-- display mode -->
<form-toggle v-on:displayMode="savePreference('displayMode', $event)" :choices="layouts" :form="preferencesForm" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
<!-- theme -->
<form-toggle v-on:theme="savePreference('theme', $event)" :choices="themes" :form="preferencesForm" fieldName="theme" :label="$t('settings.forms.theme.label')" :help="$t('settings.forms.theme.help')" />
<!-- show icon -->
<form-checkbox v-on:showAccountsIcons="savePreference('showAccountsIcons', $event)" :form="preferencesForm" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
<!-- Official icons -->
<form-checkbox v-on:getOfficialIcons="savePreference('getOfficialIcons', $event)" :form="preferencesForm" fieldName="getOfficialIcons" :label="$t('settings.forms.get_official_icons.label')" :help="$t('settings.forms.get_official_icons.help')" />
<!-- password format -->
<form-checkbox v-on:formatPassword="savePreference('formatPassword', $event)" :form="preferencesForm" fieldName="formatPassword" :label="$t('settings.forms.password_format.label')" :help="$t('settings.forms.password_format.help')" />
<form-toggle v-if="preferencesForm.formatPassword" v-on:formatPasswordBy="savePreference('formatPasswordBy', $event)" :choices="passwordFormats" :form="preferencesForm" fieldName="formatPasswordBy" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
<!-- default group -->
<form-select v-on:defaultGroup="savePreference('defaultGroup', $event)" :options="groups" :form="preferencesForm" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" />
<!-- retain active group -->
<form-checkbox v-on:rememberActiveGroup="savePreference('rememberActiveGroup', $event)" :form="preferencesForm" fieldName="rememberActiveGroup" :label="$t('settings.forms.remember_active_group.label')" :help="$t('settings.forms.remember_active_group.help')" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
<!-- auto lock -->
<form-select v-on:kickUserAfter="savePreference('kickUserAfter', $event)" :options="kickUserAfters" :form="preferencesForm" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
<!-- otp as dot -->
<form-checkbox v-on:showOtpAsDot="savePreference('showOtpAsDot', $event)" :form="preferencesForm" fieldName="showOtpAsDot" :label="$t('settings.forms.show_otp_as_dot.label')" :help="$t('settings.forms.show_otp_as_dot.help')" />
<!-- close otp on copy -->
<form-checkbox v-on:closeOtpOnCopy="savePreference('closeOtpOnCopy', $event)" :form="preferencesForm" fieldName="closeOtpOnCopy" :label="$t('settings.forms.close_otp_on_copy.label')" :help="$t('settings.forms.close_otp_on_copy.help')" />
<!-- copy otp on get -->
<form-checkbox v-on:copyOtpOnDisplay="savePreference('copyOtpOnDisplay', $event)" :form="preferencesForm" 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>
<!-- basic qrcode -->
<form-checkbox v-on:useBasicQrcodeReader="savePreference('useBasicQrcodeReader', $event)" :form="preferencesForm" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
<!-- direct capture -->
<form-checkbox v-on:useDirectCapture="savePreference('useDirectCapture', $event)" :form="preferencesForm" fieldName="useDirectCapture" :label="$t('settings.forms.useDirectCapture.label')" :help="$t('settings.forms.useDirectCapture.help')" />
<!-- default capture mode -->
<form-select v-on:defaultCaptureMode="savePreference('defaultCaptureMode', $event)" :options="captureModes" :form="preferencesForm" fieldName="defaultCaptureMode" :label="$t('settings.forms.defaultCaptureMode.label')" :help="$t('settings.forms.defaultCaptureMode.help')" />
</div>
<!-- Admin settings -->
<div v-if="settingsForm">
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.administration') }}</h4>
<div class="is-size-7-mobile block" v-html="$t('settings.administration_legend')"></div>
<!-- Check for update -->
<form-checkbox v-on:checkForUpdate="saveSetting('checkForUpdate', $event)" :form="settingsForm" fieldName="checkForUpdate" :label="$t('commons.check_for_update')" :help="$t('commons.check_for_update_help')" />
<version-checker></version-checker>
<!-- protect db -->
<form-checkbox v-on:useEncryption="saveSetting('useEncryption', $event)" :form="settingsForm" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
</div>
<!-- display mode -->
<form-toggle v-on:displayMode="saveSetting('displayMode', $event)" :choices="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
<!-- theme -->
<form-toggle v-on:theme="saveSetting('theme', $event)" :choices="themes" :form="form" fieldName="theme" :label="$t('settings.forms.theme.label')" :help="$t('settings.forms.theme.help')" />
<!-- show icon -->
<form-checkbox v-on:showAccountsIcons="saveSetting('showAccountsIcons', $event)" :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
<!-- 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')" />
<!-- password format -->
<form-checkbox v-on:formatPassword="saveSetting('formatPassword', $event)" :form="form" fieldName="formatPassword" :label="$t('settings.forms.password_format.label')" :help="$t('settings.forms.password_format.help')" />
<form-toggle v-if="form.formatPassword" v-on:formatPasswordBy="saveSetting('formatPasswordBy', $event)" :choices="passwordFormats" :form="form" fieldName="formatPasswordBy" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
<!-- 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')" />
<!-- retain active group -->
<form-checkbox v-on:rememberActiveGroup="saveSetting('rememberActiveGroup', $event)" :form="form" fieldName="rememberActiveGroup" :label="$t('settings.forms.remember_active_group.label')" :help="$t('settings.forms.remember_active_group.help')" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
<!-- auto lock -->
<form-select v-on:kickUserAfter="saveSetting('kickUserAfter', $event)" :options="kickUserAfters" :form="form" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
<!-- protect db -->
<form-checkbox v-on:useEncryption="saveSetting('useEncryption', $event)" :form="form" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
<!-- otp as dot -->
<form-checkbox v-on:showOtpAsDot="saveSetting('showOtpAsDot', $event)" :form="form" fieldName="showOtpAsDot" :label="$t('settings.forms.show_otp_as_dot.label')" :help="$t('settings.forms.show_otp_as_dot.help')" />
<!-- close otp on copy -->
<form-checkbox v-on:closeOtpOnCopy="saveSetting('closeOtpOnCopy', $event)" :form="form" fieldName="closeOtpOnCopy" :label="$t('settings.forms.close_otp_on_copy.label')" :help="$t('settings.forms.close_otp_on_copy.help')" />
<!-- 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')" />
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
<!-- 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')" />
<!-- direct capture -->
<form-checkbox v-on:useDirectCapture="saveSetting('useDirectCapture', $event)" :form="form" fieldName="useDirectCapture" :label="$t('settings.forms.useDirectCapture.label')" :help="$t('settings.forms.useDirectCapture.help')" />
<!-- default capture mode -->
<form-select v-on:defaultCaptureMode="saveSetting('defaultCaptureMode', $event)" :options="captureModes" :form="form" fieldName="defaultCaptureMode" :label="$t('settings.forms.defaultCaptureMode.label')" :help="$t('settings.forms.defaultCaptureMode.help')" />
</form>
</form-wrapper>
</div>
@ -93,26 +100,29 @@
export default {
data(){
return {
form: new Form({
lang: 'browser',
preferencesForm: new Form({
lang: '',
showOtpAsDot: null,
closeOtpOnCopy: null,
copyOtpOnDisplay: null,
useBasicQrcodeReader: null,
showAccountsIcons: null,
displayMode: '',
kickUserAfter: '',
useEncryption: null,
kickUserAfter: null,
defaultGroup: '',
useDirectCapture: null,
defaultCaptureMode: '',
rememberActiveGroup: true,
rememberActiveGroup: null,
getOfficialIcons: null,
checkForUpdate: null,
theme: 'dark',
theme: '',
formatPassword: null,
formatPasswordBy: '',
}),
settingsForm: null,
settings: {
useEncryption: null,
checkForUpdate: null,
},
layouts: [
{ text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' },
{ text: this.$t('settings.forms.list'), value: 'list', icon: 'list' },
@ -128,15 +138,15 @@
{ text: '1234 5678', value: 0.5, legend: this.$t('settings.forms.half'), title: this.$t('settings.forms.half_legend') },
],
kickUserAfters: [
{ text: this.$t('settings.forms.never'), value: '0' },
{ text: this.$t('settings.forms.on_otp_copy'), value: '-1' },
{ text: this.$t('settings.forms.1_minutes'), value: '1' },
{ text: this.$t('settings.forms.5_minutes'), value: '5' },
{ text: this.$t('settings.forms.10_minutes'), value: '10' },
{ text: this.$t('settings.forms.15_minutes'), value: '15' },
{ text: this.$t('settings.forms.30_minutes'), value: '30' },
{ text: this.$t('settings.forms.1_hour'), value: '60' },
{ text: this.$t('settings.forms.1_day'), value: '1440' },
{ text: this.$t('settings.forms.never'), value: 0 },
{ text: this.$t('settings.forms.on_otp_copy'), value: -1 },
{ text: this.$t('settings.forms.1_minutes'), value: 1 },
{ text: this.$t('settings.forms.5_minutes'), value: 5 },
{ text: this.$t('settings.forms.10_minutes'), value: 10 },
{ text: this.$t('settings.forms.15_minutes'), value: 15 },
{ text: this.$t('settings.forms.30_minutes'), value: 30 },
{ text: this.$t('settings.forms.1_hour'), value: 60 },
{ text: this.$t('settings.forms.1_day'), value: 1440 },
],
groups: [
{ text: this.$t('groups.no_group'), value: 0 },
@ -157,7 +167,7 @@
computed : {
langs: function() {
let locales = [{
text: this.$t('languages.browser_preference') + ' (' + this.$root.$i18n.locale + ')',
text: this.$t('languages.browser_preference'),
value: 'browser'
}];
@ -172,39 +182,41 @@
},
async mounted() {
const { data } = await this.form.get('/api/v1/settings')
this.form.fillWithKeyValueObject(data)
let lang = data.filter(x => x.key === 'lang')
const preferences = await this.preferencesForm.get('/api/v1/user/preferences')
this.preferencesForm.fillWithKeyValueObject(preferences.data)
this.preferencesForm.setOriginal()
if (lang.value == 'browser') {
if(window.appLocales.includes(lang.value)) {
this.form.lang = lang
}
}
// this.$root.$i18n.locale
this.axios.get('/api/v1/settings', {returnError: true}).then(response => {
this.settingsForm = new Form(this.settings)
this.settingsForm.fillWithKeyValueObject(response.data)
this.settingsForm.setOriginal()
})
.catch(error => {
// no admin rights, we do not set the Settings form
})
this.form.setOriginal()
this.fetchGroups()
},
methods : {
handleSubmit(e) {
e.preventDefault()
console.log(e)
// this.form.post('/api/v1/settings/options', {returnError: false})
// .then(response => {
savePreference(preferenceName, event) {
// this.$notify({ type: 'is-success', text: response.data.message })
this.axios.put('/api/v1/user/preferences/' + preferenceName, { value: event }).then(response => {
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
// if(response.data.settings.lang !== this.$root.$i18n.locale) {
// this.$router.go()
// }
// else {
// this.$root.appSettings = response.data.settings
// }
// });
if(preferenceName === 'lang' && response.data.value !== this.$root.$i18n.locale) {
this.$router.go()
}
else {
this.$root.userPreferences[response.data.key] = response.data.value
if(preferenceName === 'theme') {
this.setTheme(response.data.value)
}
}
})
},
saveSetting(settingName, event) {
@ -212,16 +224,7 @@
this.axios.put('/api/v1/settings/' + settingName, { value: event }).then(response => {
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
if(settingName === 'lang' && response.data.value !== this.$root.$i18n.locale) {
this.$router.go()
}
else {
this.$root.appSettings[response.data.key] = response.data.value
if(settingName === 'theme') {
this.setTheme(response.data.value)
}
}
this.$root.appSettings[response.data.key] = response.data.value
})
},

View File

@ -92,9 +92,9 @@
* Save a setting
*/
saveSetting(settingName, event) {
this.axios.put('/api/v1/settings/' + settingName, { value: event }).then(response => {
this.axios.put('/api/v1/user/preferences/' + 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
this.$root.userPreferences[response.data.key] = response.data.value
})
},
@ -191,8 +191,8 @@
if (this.credentials.length == 0) {
this.form.useWebauthnOnly = false
this.form.useWebauthnAsDefault = false
this.$root.appSettings['useWebauthnOnly'] = false
this.$root.appSettings['useWebauthnAsDefault'] = false
this.$root.userPreferences['useWebauthnOnly'] = false
this.$root.userPreferences['useWebauthnAsDefault'] = false
}
this.$notify({ type: 'is-success', text: this.$t('auth.webauthn.device_revoked') })

View File

@ -65,7 +65,7 @@
<label class="label">{{ $t('twofaccounts.icon') }}</label>
<div class="field is-grouped">
<!-- i'm lucky button -->
<div class="control" v-if="$root.appSettings.getOfficialIcons">
<div class="control" v-if="$root.userPreferences.getOfficialIcons">
<v-button @click="fetchLogo" :color="$root.showDarkMode ? 'is-dark' : ''" :nativeType="'button'" :isDisabled="form.service.length < 1">
<span class="icon is-small">
<font-awesome-icon :icon="['fas', 'globe']" />
@ -94,7 +94,7 @@
</div>
<div class="field">
<field-error :form="form" field="icon" class="help-for-file" />
<p v-if="$root.appSettings.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
<p v-if="$root.userPreferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
</div>
<!-- otp type -->
<form-toggle class="has-uppercased-button" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
@ -365,7 +365,7 @@
},
fetchLogo() {
if (this.$root.appSettings.getOfficialIcons) {
if (this.$root.userPreferences.getOfficialIcons) {
this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
if (response.status === 201) {
// clean possible already uploaded temp icon
@ -397,7 +397,7 @@
clipboardSuccessHandler ({ value, event }) {
if(this.$root.appSettings.kickUserAfter == -1) {
if(this.$root.appSettings.userPreferences == -1) {
this.appLogout()
}

View File

@ -9,7 +9,7 @@
<label class="label">{{ $t('twofaccounts.icon') }}</label>
<div class="field is-grouped">
<!-- i'm lucky button -->
<div class="control" v-if="$root.appSettings.getOfficialIcons">
<div class="control" v-if="$root.userPreferences.getOfficialIcons">
<v-button @click="fetchLogo" :color="$root.showDarkMode ? 'is-dark' : ''" :nativeType="'button'" :isDisabled="form.service.length < 3">
<span class="icon is-small">
<font-awesome-icon :icon="['fas', 'globe']" />
@ -38,7 +38,7 @@
</div>
<div class="field">
<field-error :form="form" field="icon" class="help-for-file" />
<p v-if="$root.appSettings.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
<p v-if="$root.userPreferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
</div>
<!-- otp type -->
<form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
@ -258,7 +258,7 @@
},
fetchLogo() {
if (this.$root.appSettings.getOfficialIcons) {
if (this.$root.userPreferences.getOfficialIcons) {
this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
if (response.status === 201) {
// clean possible already uploaded temp icon

View File

@ -55,7 +55,7 @@
<div class="is-flex is-justify-content-space-between">
<!-- Account name -->
<div v-if="account.id > -2 && account.imported !== 0" class="is-flex-grow-1 has-ellipsis is-clickable" @click="previewAccount(index)" :title="$t('twofaccounts.import.generate_a_test_password')">
<img v-if="account.icon && $root.appSettings.showAccountsIcons" class="import-icon" :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
<img v-if="account.icon && $root.userPreferences.showAccountsIcons" class="import-icon" :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
{{ account.account }}
</div>
<div v-else class="is-flex-grow-1 has-ellipsis">{{ account.account }}</div>

View File

@ -47,5 +47,7 @@
'unsupported_otp_type' => 'Unsupported OTP type',
'encrypted_migration' => 'Unreadable, the data seem encrypted',
'no_logo_found_for_x' => 'No logo available for {service}',
'file_upload_failed' => 'File upload failed'
'file_upload_failed' => 'File upload failed',
'unauthorized' => 'Unauthorized',
'unauthorized_legend' => 'You do not have permissions to view this resource or to perform this action',
];

View File

@ -14,6 +14,7 @@
*/
'settings' => 'Settings',
'preferences' => 'Preferences',
'account' => 'Account',
'oauth' => 'OAuth',
'webauthn' => 'WebAuthn',
@ -23,6 +24,9 @@
'confirm' => [
],
'administration' => 'Administration',
'administration_legend' => 'While previous settings are user settings (every user can set its own preferences), following settings are global and apply to all users. Only an administrator can view and edit those settings.',
'you_are_administrator' => 'You are an administrator',
'general' => 'General',
'security' => 'Security',
'profile' => 'Profile',

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<html data-theme='{{ $theme }}' lang="{!! $lang !!}">
<html data-theme="{{ $userPreferences['theme'] }}" lang="{{ $userPreferences['lang'] }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="{{ __('commons.2fauth_description') }}" lang="{!! $lang !!}">
<meta name="description" content="{{ __('commons.2fauth_description') }}" lang="{{ $userPreferences['lang'] }}">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no, viewport-fit=cover">
<meta name="csrf-token" content="{{csrf_token()}}">
<meta name="robots" content="noindex, nofollow">
@ -26,6 +26,7 @@
<script type="text/javascript">
var appSettings = {!! $appSettings !!};
var appConfig = {!! $appConfig !!};
var userPreferences = {!! $userPreferences->toJson() !!};
var appVersion = '{{ config("2fauth.version") }}';
var isDemoApp = {!! $isDemoApp !!};
var isTestingApp = {!! $isTestingApp !!};

View File

@ -25,11 +25,9 @@
Route::group(['middleware' => 'auth:api-guard'], function () {
Route::get('user', [UserController::class, 'show'])->name('user.show'); // Returns email address in addition to the username
Route::get('settings/{settingName}', [SettingController::class, 'show'])->name('settings.show');
Route::get('settings', [SettingController::class, 'index'])->name('settings.index');
Route::post('settings', [SettingController::class, 'store'])->name('settings.store');
Route::put('settings/{settingName}', [SettingController::class, 'update'])->name('settings.update');
Route::delete('settings/{settingName}', [SettingController::class, 'destroy'])->name('settings.destroy');
Route::get('user/preferences/{preferenceName}', [UserController::class, 'showPreference'])->name('user.preferences.show');
Route::get('user/preferences', [UserController::class, 'allPreferences'])->name('user.preferences.all');
Route::put('user/preferences/{preferenceName}', [UserController::class, 'setPreference'])->name('user.preferences.set');
Route::delete('twofaccounts', [TwoFAccountController::class, 'batchDestroy'])->name('twofaccounts.batchDestroy');
Route::patch('twofaccounts/withdraw', [TwoFAccountController::class, 'withdraw'])->name('twofaccounts.withdraw');
@ -53,3 +51,15 @@
Route::post('icons', [IconController::class, 'upload'])->name('icons.upload');
Route::delete('icons/{icon}', [IconController::class, 'delete'])->name('icons.delete');
});
/**
* Routes protected by the api authentication guard and restricted to administrators
*/
Route::group(['middleware' => ['auth:api-guard', 'admin']], function () {
Route::get('settings/{settingName}', [SettingController::class, 'show'])->name('settings.show');
Route::get('settings', [SettingController::class, 'index'])->name('settings.index');
Route::post('settings', [SettingController::class, 'store'])->name('settings.store');
Route::put('settings/{settingName}', [SettingController::class, 'update'])->name('settings.update');
Route::delete('settings/{settingName}', [SettingController::class, 'destroy'])->name('settings.destroy');
});

View File

@ -152,9 +152,9 @@ public function test_get_float_setting_returns_float()
/**
* @test
*/
public function test_all_returns_native_and_user_settings()
public function test_all_returns_default_and_overloaded_settings()
{
$native_options = config('2fauth.options');
$default_options = config('2fauth.settings');
Settings::set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
@ -163,12 +163,10 @@ public function test_all_returns_native_and_user_settings()
$this->assertArrayHasKey(self::SETTING_NAME, $all);
$this->assertEquals($all[self::SETTING_NAME], self::SETTING_VALUE_STRING);
foreach ($native_options as $key => $val) {
foreach ($default_options as $key => $val) {
$this->assertArrayHasKey($key, $all);
$this->assertEquals($all[$key], $val);
}
$this->assertArrayHasKey('lang', $all);
}
/**
@ -347,23 +345,23 @@ public function test_del_remove_setting_from_db_and_cache()
/**
* @test
*/
public function test_isUserDefined_returns_true()
public function test_isEdited_returns_true()
{
DB::table('options')->insert(
[self::KEY => 'showTokenAsDot', self::VALUE => strval(self::SETTING_VALUE_TRUE_TRANSFORMED)]
);
$this->assertTrue(Settings::isUserDefined('showTokenAsDot'));
$this->assertTrue(Settings::isEdited('showTokenAsDot'));
}
/**
* @test
*/
public function test_isUserDefined_returns_false()
public function test_isEdited_returns_false()
{
DB::table('options')->where(self::KEY, 'showTokenAsDot')->delete();
$this->assertFalse(Settings::isUserDefined('showTokenAsDot'));
$this->assertFalse(Settings::isEdited('showTokenAsDot'));
}
/**