Login improve UI config (#2171)

* feat: show auth configs on the admin UI
* fix: do not access settings on load
* fix: redirect for click on item (not only on text)
* fix: remove `Display Name`
* fix: do not show `Verify email with` if no auth option is available
* feat: show warning before logout
* feat: i18n of account page
* fix: show account icon for user ID login
* fix: always check `isUserAuthorized`
* fix: update the `disclaimer_dialog` message
* feat: hide user ID by default
* fix: redirect from login page when user authorized
* feat: update logout message
* fix: do not translate company names
This commit is contained in:
Vlad Stan 2023-12-14 12:34:23 +02:00 committed by GitHub
parent 24b02cc656
commit bb918a8523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 339 additions and 215 deletions

View File

@ -1,62 +1,145 @@
<q-tab-panel name="users">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">User Management</h6>
<br />
<div>
<p>Admin Users</p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
label="User ID"
hint="Users with admin privileges"
>
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{%raw%}
<q-chip
v-for="user in formData.lnbits_admin_users"
:key="user"
removable
@remove="removeAdminUser(user)"
color="primary"
text-color="white"
<h6 class="q-my-none q-mb-sm">User Management</h6>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p>Admin Users</p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
label="User ID"
hint="Users with admin privileges"
>
{{ user }}
</q-chip>
{%endraw%}
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{%raw%}
<q-chip
v-for="user in formData.lnbits_admin_users"
:key="user"
removable
@remove="removeAdminUser(user)"
color="primary"
text-color="white"
>
{{ user }}
</q-chip>
{%endraw%}
</div>
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>Allowed Users</p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
label="User ID"
hint="Only these users can use LNbits"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{% raw %}
<q-chip
v-for="user in formData.lnbits_allowed_users"
:key="user"
removable
@remove="removeAllowedUser(user)"
color="primary"
text-color="white"
>
{{ user }}
</q-chip>
{% endraw %}
</div>
<br />
</div>
<br />
</div>
<div>
<p>Allowed Users</p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
label="User ID"
hint="Only these users can use LNbits"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{% raw %}
<q-chip
v-for="user in formData.lnbits_allowed_users"
:key="user"
removable
@remove="removeAllowedUser(user)"
color="primary"
text-color="white"
</q-card-section>
<q-separator></q-separator>
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">Authentication</h6>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.auth_token_expire_minutes"
type="number"
label="Token expire minutes"
hint="Time in minutes until the token expires"
>
{{ user }}
</q-chip>
{% endraw %}
</q-input>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-select
filled
v-model="formData.auth_allowed_methods"
multiple
hint="Allowed authorization methods"
label="Select authorization methods"
:options="formData.auth_all_methods"
></q-select>
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('google-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">Google Auth</strong>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.google_client_id"
label="Google Client ID"
hint="Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token"
>
</q-input>
</div>
<div class="col-md-6 col-sm-12">
<q-input
filled
v-model="formData.google_client_secret"
type="password"
label="Google Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('github-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">GitHub Auth</strong>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.github_client_id"
label="GitHub Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token"
>
</q-input>
</div>
<div class="col-md-6 col-sm-12">
<q-input
filled
v-model="formData.github_client_secret"
type="password"
label="GitHub Client Secret"
>
</q-input>
</div>
<br />
</div>
</q-card-section>
</q-tab-panel>

View File

@ -11,7 +11,9 @@
<q-card-section>
<div class="row">
<div class="col">
<h4 class="q-my-none">Password Settings</h4>
<h4 class="q-my-none">
<span v-text="$t('password_config')"></span>
</h4>
</div>
<div class="col">
<q-img
@ -33,26 +35,26 @@
label="Old Password"
filled
dense
:rules="[(val) => !val || val.length >= 8 || 'Password must have at least 8 characters']"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
v-model="passwordData.newPassword"
type="password"
autocomplete="off"
label="New Password"
:label="$t('password')"
filled
dense
:rules="[(val) => !val || val.length >= 8 || 'Password must have at least 8 characters']"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
v-model="passwordData.newPasswordRepeat"
type="password"
autocomplete="off"
label="New Password Repeat"
:label="$t('password_repeat')"
filled
dense
class="q-mb-md"
:rules="[(val) => !val || val.length >= 8 || 'Password must have at least 8 characters']"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
</q-card-section>
<q-separator></q-separator>
@ -62,11 +64,12 @@
:disable="(!passwordData.newPassword || !passwordData.newPasswordRepeat) || passwordData.newPassword !== passwordData.newPasswordRepeat"
unelevated
color="primary"
>Update Password</q-btn
:label="$t('change_password')"
>
</q-btn>
<q-btn
@click="passwordData.show = false"
label="Back"
:label="$t('back')"
outline
unelevated
color="grey"
@ -78,7 +81,9 @@
<q-card-section>
<div class="row">
<div class="col">
<h4 class="q-my-none">Account Settings</h4>
<h4 class="q-my-none">
<span v-text="$t('account_settings')"></span>
</h4>
</div>
<div class="col">
<q-img
@ -95,16 +100,23 @@
<q-card-section>
<q-input
v-model="user.id"
label="User ID"
:label="$t('user_id')"
filled
dense
readonly
:type="showUserId ? 'text': 'password'"
class="q-mb-md"
>
><q-btn
@click="showUserId = !showUserId"
dense
flat
:icon="showUserId ? 'visibility_off' : 'visibility'"
color="grey"
></q-btn>
</q-input>
<q-input
v-model="user.username"
label="Username"
:label="$t('username')"
filled
dense
:readonly="hasUsername"
@ -113,7 +125,7 @@
</q-input>
<q-input
v-model="user.email"
label="Email"
:label="$t('email')"
filled
dense
readonly
@ -122,8 +134,12 @@
</q-input>
<div v-if="!user.email" class="row"></div>
<div v-if="!user.email" class="row">
<div class="col q-pa-sm text-h6">Check email using:</div>
{% if "google-auth" in LNBITS_AUTH_METHODS %}
{% if "google-auth" in LNBITS_AUTH_METHODS or "github-auth" in
LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm text-h6">
<span v-text="$t('verify_email')"></span>:
</div>
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<q-btn
:href="`/api/v1/auth/google?user_id=${user.id}`"
@ -166,17 +182,9 @@
</q-card-section>
<q-card-section v-if="user.config">
<q-input
v-model="user.config.display_name"
label="Display Name"
filled
dense
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.config.first_name"
label="First Name"
:label="$t('first_name')"
filled
dense
class="q-mb-md"
@ -184,7 +192,7 @@
</q-input>
<q-input
v-model="user.config.last_name"
label="Last Name"
:label="$t('last_name')"
filled
dense
class="q-mb-md"
@ -192,7 +200,7 @@
</q-input>
<q-input
v-model="user.config.provider"
label="Auth Provider"
:label="$t('auth_provider')"
filled
dense
readonly
@ -201,7 +209,7 @@
</q-input>
<q-input
v-model="user.config.picture"
label="Picture"
:label="$t('picture')"
filled
dense
class="q-mb-md"
@ -210,12 +218,12 @@
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-btn @click="updateAccount" unelevated color="primary"
>Update Account</q-btn
>
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
<q-btn
@click="showChangePassword()"
:label="user.has_password ? 'Change Password': 'Set Password'"
:label="user.has_password ? $t('change_password'): $t('set_password')"
outline
unelevated
color="grey"
@ -227,7 +235,7 @@
<div v-else class="col-12 col-md-6 q-gutter-y-md">
<q-card>
<q-card-section>
<h4 class="q-my-none">Account</h4>
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
</q-card-section>
</q-card>
</div>

View File

@ -7,7 +7,7 @@
class="col-12 col-md-7 col-lg-6 q-gutter-y-md"
></div>
<div v-else class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<div>
<div class="gt-sm">
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
</div>
@ -32,7 +32,7 @@
</div>
{%else%} {% endif %}
<div class="row q-mt-xl">
<div class="row q-mt-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-badge v-if="isAccessTokenExpired" color="primary" rounded>
<div class="text-h5">
@ -218,7 +218,7 @@
>
<div class="row">
{% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/google"
type="a"
@ -239,7 +239,7 @@
</q-btn>
</div>
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/github"
type="a"
@ -281,7 +281,10 @@
</div>
<!-- Ads -->
<div class="col-12 col-md-3 col-lg-3" v-if="'{{SITE_TITLE}}' == 'LNbits'">
<div
class="col-12 col-md-3 col-lg-3 gt-sm"
v-if="'{{SITE_TITLE}}' == 'LNbits'"
>
<div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn
@ -480,4 +483,11 @@
</div>
</div>
</div>
<div class="row gt-sm q-mt-xl">
<div class="col-1"></div>
<div class="col-10 q-pl-xl">
<span v-text="$t('lnbits_description')"></span>
</div>
<div class="col-1"></div>
</div>
{% endblock %}

View File

@ -878,8 +878,9 @@
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
:label="$t('copy_wallet_url')"
type="a"
href="/account"
:label="$t('my_account')"
></q-btn>
<q-btn
v-close-popup

View File

@ -48,38 +48,6 @@ from ..models import (
auth_router = APIRouter()
def _init_google_sso() -> Optional[GoogleSSO]:
if not settings.is_auth_method_allowed(AuthMethods.google_auth):
return None
if not settings.is_google_auth_configured:
logger.warning("Google Auth allowed but not configured.")
return None
return GoogleSSO(
settings.google_client_id,
settings.google_client_secret,
None,
allow_insecure_http=True,
)
def _init_github_sso() -> Optional[GithubSSO]:
if not settings.is_auth_method_allowed(AuthMethods.github_auth):
return None
if not settings.is_github_auth_configured:
logger.warning("Github Auth allowed but not configured.")
return None
return GithubSSO(
settings.github_client_id,
settings.github_client_secret,
None,
allow_insecure_http=True,
)
google_sso = _init_google_sso()
github_sso = _init_github_sso()
@auth_router.get("/api/v1/auth", description="Get the authenticated user")
async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
return user
@ -128,6 +96,7 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
@auth_router.get("/api/v1/auth/google", description="Google SSO")
async def login_with_google(request: Request, user_id: Optional[str] = None):
google_sso = _new_google_sso()
if not google_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.")
@ -139,6 +108,7 @@ async def login_with_google(request: Request, user_id: Optional[str] = None):
@auth_router.get("/api/v1/auth/github", description="Github SSO")
async def login_with_github(request: Request, user_id: Optional[str] = None):
github_sso = _new_github_sso()
if not github_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.")
@ -152,6 +122,7 @@ async def login_with_github(request: Request, user_id: Optional[str] = None):
"/api/v1/auth/google/token", description="Handle Google OAuth callback"
)
async def handle_google_token(request: Request) -> RedirectResponse:
google_sso = _new_google_sso()
if not google_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.")
@ -177,6 +148,7 @@ async def handle_google_token(request: Request) -> RedirectResponse:
"/api/v1/auth/github/token", description="Handle Github OAuth callback"
)
async def handle_github_token(request: Request) -> RedirectResponse:
github_sso = _new_github_sso()
if not github_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.")
@ -336,6 +308,34 @@ def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
return response
def _new_google_sso() -> Optional[GoogleSSO]:
if not settings.is_auth_method_allowed(AuthMethods.google_auth):
return None
if not settings.is_google_auth_configured:
logger.warning("Google Auth allowed but not configured.")
return None
return GoogleSSO(
settings.google_client_id,
settings.google_client_secret,
None,
allow_insecure_http=True,
)
def _new_github_sso() -> Optional[GithubSSO]:
if not settings.is_auth_method_allowed(AuthMethods.github_auth):
return None
if not settings.is_github_auth_configured:
logger.warning("Github Auth allowed but not configured.")
return None
return GithubSSO(
settings.github_client_id,
settings.github_client_secret,
None,
allow_insecure_http=True,
)
def _encrypt_message(m: Optional[str] = None) -> Optional[str]:
if not m:
return None

View File

@ -247,6 +247,45 @@ class NodeUISettings(LNbitsSettings):
lnbits_node_ui_transactions: bool = Field(default=False)
class AuthMethods(Enum):
user_id_only = "user-id-only"
username_and_password = "username-password"
google_auth = "google-auth"
github_auth = "github-auth"
class AuthSettings(LNbitsSettings):
auth_token_expire_minutes: int = Field(default=525600)
auth_all_methods = [a.value for a in AuthMethods]
auth_allowed_methods: List[str] = Field(
default=[
AuthMethods.user_id_only.value,
AuthMethods.username_and_password.value,
]
)
def is_auth_method_allowed(self, method: AuthMethods):
return method.value in self.auth_allowed_methods
class GoogleAuthSettings(LNbitsSettings):
google_client_id: str = Field(default="")
google_client_secret: str = Field(default="")
@property
def is_google_auth_configured(self):
return self.google_client_id != "" and self.google_client_secret != ""
class GitHubAuthSettings(LNbitsSettings):
github_client_id: str = Field(default="")
github_client_secret: str = Field(default="")
@property
def is_github_auth_configured(self):
return self.github_client_id != "" and self.github_client_secret != ""
class EditableSettings(
UsersSettings,
ExtensionsSettings,
@ -257,6 +296,9 @@ class EditableSettings(
LightningSettings,
WebPushSettings,
NodeUISettings,
AuthSettings,
GoogleAuthSettings,
GitHubAuthSettings,
):
@validator(
"lnbits_admin_users",
@ -298,6 +340,7 @@ class EnvSettings(LNbitsSettings):
lnbits_path: str = Field(default=".")
lnbits_extensions_path: str = Field(default="lnbits")
super_user: str = Field(default="")
auth_secret_key: str = Field(default="")
version: str = Field(default="0.0.0")
user_agent: str = Field(default="")
enable_log_to_file: bool = Field(default=True)
@ -310,45 +353,6 @@ class EnvSettings(LNbitsSettings):
return self.lnbits_extensions_path == "lnbits"
class AuthMethods(Enum):
user_id_only = "user-id-only"
username_and_password = "username-password"
google_auth = "google-auth"
github_auth = "github-auth"
class AuthSettings(LNbitsSettings):
auth_secret_key: str = Field(default="")
auth_token_expire_minutes: int = Field(default=30)
auth_allowed_methods: List[str] = Field(
default=[
AuthMethods.user_id_only.value,
AuthMethods.username_and_password.value,
]
)
def is_auth_method_allowed(self, method: AuthMethods):
return method.value in self.auth_allowed_methods
class GoogleAuthSettings(LNbitsSettings):
google_client_id: str = Field(default="")
google_client_secret: str = Field(default="")
@property
def is_google_auth_configured(self):
return self.google_client_id != "" and self.google_client_secret != ""
class GitHubAuthSettings(LNbitsSettings):
github_client_id: str = Field(default="")
github_client_secret: str = Field(default="")
@property
def is_github_auth_configured(self):
return self.github_client_id != "" and self.github_client_secret != ""
class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: Optional[str] = Field(default=None)
lnbits_saas_secret: Optional[str] = Field(default=None)
@ -397,9 +401,6 @@ class ReadOnlySettings(
SaaSSettings,
PersistenceSettings,
SuperUserSettings,
AuthSettings,
GoogleAuthSettings,
GitHubAuthSettings,
):
lnbits_admin_ui: bool = Field(default=False)

File diff suppressed because one or more lines are too long

View File

@ -96,7 +96,7 @@ window.localisation.en = {
i_understand: 'I understand',
copy_wallet_url: 'Copy wallet URL',
disclaimer_dialog:
'Login functionality to be released in a future update, for now, make sure you bookmark this page for future access to your wallet! This service is in BETA, and we hold no responsibility for people losing access to funds.',
'To ensure continuous access to your wallets, please remember to securely store your login credentials! Please visit the "My Account" page. This service is in BETA, and we hold no responsibility for people losing access to funds.',
no_transactions: 'No transactions made yet',
manage: 'Manage',
extensions: 'Extensions',
@ -197,15 +197,30 @@ window.localisation.en = {
create_new_wallet: 'Create New Wallet',
login_to_account: 'Login to your account',
create_account: 'Create account',
account_settings: 'Account Settings',
signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub',
username_or_email: 'Username or Email',
password: 'Password',
password_config: 'Password Config',
password_repeat: 'Password repeat',
change_password: 'Change Password',
set_password: 'Set Password',
invalid_password: 'Password must have at least 8 characters',
login: 'Login',
register: 'Register',
username: 'Username',
user_id: 'User ID',
email: 'Email',
first_name: 'First Name',
last_name: 'Last Name',
picture: 'Picture',
verify_email: 'Verify email with',
account: 'Account',
update_account: 'Update Account',
invalid_username: 'Invalid Username',
back: 'Back'
auth_provider: 'Auth Provider',
my_account: 'My Account',
back: 'Back',
logout: 'Logout'
}

View File

@ -5,6 +5,7 @@ new Vue({
return {
user: null,
hasUsername: false,
showUserId: false,
passwordData: {
show: false,
oldPassword: null,

View File

@ -407,6 +407,7 @@ window.windowMixin = {
data: function () {
return {
toggleSubs: true,
isUserAuthorized: false,
g: {
offline: !navigator.onLine,
visibleDrawer: false,
@ -420,12 +421,6 @@ window.windowMixin = {
}
},
computed: {
isUserAuthorized() {
return this.$q.cookies.get('is_lnbits_user_authorized')
}
},
methods: {
activeLanguage: function (lang) {
return window.i18n.locale === lang
@ -452,31 +447,45 @@ window.windowMixin = {
})
},
checkUsrInUrl: async function () {
const params = new URLSearchParams(window.location.search)
const usr = params.get('usr')
if (!usr) {
return
}
try {
const params = new URLSearchParams(window.location.search)
const usr = params.get('usr')
if (!usr) {
return
}
if (!this.isUserAuthorized) {
await LNbits.api.loginUsr(usr)
}
params.delete('usr')
const cleanQueryPrams = params.size ? `?${params.toString()}` : ''
if (!this.isUserAuthorized) {
await LNbits.api.loginUsr(usr)
}
window.history.replaceState(
{},
document.title,
window.location.pathname + cleanQueryPrams
)
params.delete('usr')
const cleanQueryPrams = params.size ? `?${params.toString()}` : ''
window.history.replaceState(
{},
document.title,
window.location.pathname + cleanQueryPrams
)
} finally {
this.isUserAuthorized = !!this.$q.cookies.get(
'is_lnbits_user_authorized'
)
}
},
logout: async function () {
try {
await LNbits.api.logout()
window.location = '/'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
LNbits.utils
.confirmDialog(
'Do you really want to logout?' +
' Please visit "My Account" page to check your credentials!'
)
.onOk(async () => {
try {
await LNbits.api.logout()
window.location = '/'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
})
}
},
created: async function () {

View File

@ -8,6 +8,7 @@ new Vue({
data: {},
description: ''
},
isUserAuthorized: false,
authAction: 'login',
authMethod: 'username-password',
usr: '',
@ -23,9 +24,6 @@ new Vue({
formatDescription() {
return LNbits.utils.convertMarkdown(this.description)
},
isUserAuthorized() {
return this.$q.cookies.get('is_lnbits_user_authorized')
},
isAccessTokenExpired() {
return this.$q.cookies.get('is_access_token_expired')
}
@ -96,6 +94,7 @@ new Vue({
created() {
this.description = SITE_DESCRIPTION
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
if (this.isUserAuthorized) {
window.location.href = '/wallet'
}

View File

@ -1,6 +1,6 @@
// update cache version every time there is a new deployment
// so the service worker reinitializes the cache
const CACHE_VERSION = 88
const CACHE_VERSION = 95
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {

View File

@ -266,18 +266,13 @@
</div>
</template>
<q-list>
<q-item clickable v-close-popup
<q-item tag="a" href="/account" clickable v-close-popup
><q-item-section>
<q-icon name="person" />
</q-item-section>
<q-item-section>
<!-- todo: no word break -->
<q-item-label>
<a
style="text-decoration: none; color: inherit"
href="/account"
><span> My Account</span></a
>
<span v-text="$t('my_account')"></span>
</q-item-label>
</q-item-section>
<q-item-section>
@ -290,7 +285,9 @@
<q-icon name="logout" />
</q-item-section>
<q-item-section>
<q-item-label> Logout </q-item-label>
<q-item-label>
<span v-text="$t('logout')"></span>
</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label> </q-item-label>