mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
starting profile page
This commit is contained in:
parent
d04e9a1af3
commit
f0e3d172e7
5 changed files with 541 additions and 71 deletions
486
src/ui/client/builder/pages/profile.py
Normal file
486
src/ui/client/builder/pages/profile.py
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
# Define data to put in profile widgets
|
||||
# - username
|
||||
# - email
|
||||
# - created_method
|
||||
# - is_superadmin
|
||||
# - role
|
||||
# - role_description
|
||||
# - permissions (liste of permissions [])
|
||||
# - creation_date
|
||||
# - last_update (last time update user info)
|
||||
|
||||
# Define data to put in profile form widgets
|
||||
# - update password
|
||||
# - update email
|
||||
|
||||
# Define data to put in totp widgets
|
||||
# if want to enable totp (currently disabled):
|
||||
# text with state
|
||||
# show QRcode SVG
|
||||
# - form (endpoint /totp-enable) : totp secret (type password), totp code, password
|
||||
# Case currently enabled :
|
||||
# text with state
|
||||
# after first totp setup, show recovery codes
|
||||
# form refresh recovery codes button that will redisplay recovery (endpoint /totp-refresh) : password (warning that will remove previous)
|
||||
# form disabled (endpoint /totp-disable) : totp code || recovery code, password
|
||||
|
||||
from .utils.widgets import (
|
||||
button_widget,
|
||||
button_group_widget,
|
||||
title_widget,
|
||||
subtitle_widget,
|
||||
text_widget,
|
||||
tabulator_widget,
|
||||
input_widget,
|
||||
icons_widget,
|
||||
regular_widget,
|
||||
unmatch_widget,
|
||||
pairs_widget,
|
||||
)
|
||||
from .utils.table import add_column
|
||||
from .utils.format import get_fields_from_field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def fallback_message(msg: str, display: Optional[list] = None) -> dict:
|
||||
|
||||
return {
|
||||
"type": "void",
|
||||
"display": display if display else [],
|
||||
"widgets": [
|
||||
unmatch_widget(text=msg),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def profile_info(user_profile: Optional[list] = None) -> dict:
|
||||
|
||||
if user_profile is None or len(user_profile) == 0:
|
||||
return fallback_message("profile_info_not_found", display=["main", 0])
|
||||
|
||||
return {
|
||||
"type": "card",
|
||||
"maxWidthScreen": "md",
|
||||
"display": ["main", 0],
|
||||
"widgets": [
|
||||
title_widget(
|
||||
title="profile_info_title", # keep it (a18n)
|
||||
),
|
||||
subtitle_widget(
|
||||
subtitle="profile__info_subtitle", # keep it (a18n)
|
||||
),
|
||||
pairs_widget(pairs=user_profile),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def profile_account_form(email: str) -> dict:
|
||||
return {
|
||||
"type": "card",
|
||||
"maxWidthScreen": "md",
|
||||
"display": ["main", 1],
|
||||
"widgets": [
|
||||
title_widget(
|
||||
title="profile_account_title", # keep it (a18n)
|
||||
),
|
||||
subtitle_widget(
|
||||
subtitle="profile_account_subtitle", # keep it (a18n)
|
||||
),
|
||||
regular_widget(
|
||||
maxWidthScreen="xs",
|
||||
endpoint="/totp-enable",
|
||||
method="POST",
|
||||
fields=[
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-email",
|
||||
name="email",
|
||||
type="password",
|
||||
label="profile_email", # keep it (a18n)
|
||||
value=email,
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_email_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_email_desc",
|
||||
}
|
||||
],
|
||||
)
|
||||
),
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-password",
|
||||
name="new_password",
|
||||
label="profile_password", # keep it (a18n)
|
||||
value="",
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_password_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "exclamation",
|
||||
"color": "yellow-darker",
|
||||
"text": "profile_password_warning_desc",
|
||||
},
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_password_desc",
|
||||
},
|
||||
],
|
||||
)
|
||||
),
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-password-confirm",
|
||||
name="new_password_confirm",
|
||||
label="profile_password_confirm", # keep it (a18n)
|
||||
value="",
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_password_confirm_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "exclamation",
|
||||
"color": "yellow-darker",
|
||||
"text": "profile_password_confirm_warning_desc",
|
||||
},
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_password_confirm_desc",
|
||||
},
|
||||
],
|
||||
)
|
||||
),
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-password",
|
||||
name="current_password",
|
||||
label="profile_current_password", # keep it (a18n)
|
||||
value="",
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_current_password_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "exclamation",
|
||||
"color": "yellow-darker",
|
||||
"text": "profile_current_password_warning_desc",
|
||||
},
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_current_password_desc",
|
||||
},
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
buttons=[
|
||||
button_widget(
|
||||
id="profile-account-submit",
|
||||
text="action_update",
|
||||
iconName="plus",
|
||||
iconColor="white",
|
||||
color="success",
|
||||
size="normal",
|
||||
type="submit",
|
||||
containerClass="flex justify-center",
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def totp_enable_form(
|
||||
totp_recovery_codes: Optional[list] = None,
|
||||
is_recovery_refreshed: bool = False,
|
||||
) -> dict:
|
||||
|
||||
recovery_widgets = []
|
||||
|
||||
if is_recovery_refreshed and len(totp_recovery_codes) > 0:
|
||||
recovery_widgets.append(pairs_widget(pairs=totp_recovery_codes))
|
||||
|
||||
if is_recovery_refreshed and totp_recovery_codes is None or len(totp_recovery_codes) == 0:
|
||||
recovery_widgets.append(text_widget(text="profile_recovery_codes_refresh_but_not_found", color="error", iconName="", iconColor=""))
|
||||
|
||||
recovery_widgets.append(
|
||||
button_group_widget(
|
||||
buttons=[
|
||||
button_widget(
|
||||
id="profile-disable-submit",
|
||||
text="profile_refresh_recovery_codes",
|
||||
iconName="refresh",
|
||||
iconColor="white",
|
||||
color="success",
|
||||
size="normal",
|
||||
attrs={
|
||||
"data-submit-form": "{}",
|
||||
"data-submit-endpoint": "/totp-refresh",
|
||||
"data-submit-method": "POST",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "card",
|
||||
"maxWidthScreen": "md",
|
||||
"display": ["main", 2],
|
||||
"widgets": [
|
||||
title_widget(
|
||||
title="profile_totp_title", # keep it (a18n)
|
||||
),
|
||||
subtitle_widget(
|
||||
subtitle="profile_totp_subtitle", # keep it (a18n)
|
||||
),
|
||||
text_widget(
|
||||
text="profile_totp_enable", # keep it (a18n)
|
||||
icon="check",
|
||||
textClass="flex justify-center",
|
||||
),
|
||||
# totp secret (type password), totp code, password
|
||||
regular_widget(
|
||||
title="profile_totp_disable_title",
|
||||
subtitle="profile_totp_disable_subtitle",
|
||||
maxWidthScreen="xs",
|
||||
endpoint="/totp-disable",
|
||||
method="POST",
|
||||
fields=[
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-totp-code",
|
||||
name="totp_code",
|
||||
label="profile_totp_code", # keep it (a18n)
|
||||
value="",
|
||||
isClipboard=True,
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_totp_code_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_totp_code_desc",
|
||||
}
|
||||
],
|
||||
)
|
||||
),
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-totp-code",
|
||||
name="current_password",
|
||||
label="profile_current_password", # keep it (a18n)
|
||||
value="",
|
||||
isClipboard=True,
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_current_password_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_current_password_desc",
|
||||
}
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
buttons=[
|
||||
button_widget(
|
||||
id="profile-disable-submit",
|
||||
text="action_enable",
|
||||
iconName="plus",
|
||||
iconColor="check",
|
||||
color="success",
|
||||
size="normal",
|
||||
type="submit",
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def totp_disable_form(totp_img: str = "", totp_secret: str = "") -> dict:
|
||||
return {
|
||||
"type": "card",
|
||||
"maxWidthScreen": "md",
|
||||
"display": ["main", 2],
|
||||
"widgets": [
|
||||
title_widget(
|
||||
title="profile_totp_title", # keep it (a18n)
|
||||
),
|
||||
subtitle_widget(
|
||||
subtitle="profile_totp_subtitle", # keep it (a18n)
|
||||
),
|
||||
text_widget(
|
||||
text="profile_totp_disable", # keep it (a18n)
|
||||
icon="cross",
|
||||
textClass="flex justify-center",
|
||||
),
|
||||
image_widget(
|
||||
src=totp_img,
|
||||
alt="profile_totp_qr_code", # keep it (a18n)
|
||||
),
|
||||
# totp secret (type password), totp code, password
|
||||
regular_widget(
|
||||
maxWidthScreen="xs",
|
||||
endpoint="/edit",
|
||||
method="POST",
|
||||
fields=[
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-totp-secret",
|
||||
name="totp_secret",
|
||||
label="profile_totp_secret", # keep it (a18n)
|
||||
value=totp_secret,
|
||||
type="password",
|
||||
isClipboard=True,
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_totp_secret_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_totp_secret_desc",
|
||||
}
|
||||
],
|
||||
)
|
||||
),
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-totp-code",
|
||||
name="totp_code",
|
||||
label="profile_totp_code", # keep it (a18n)
|
||||
value="",
|
||||
isClipboard=True,
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_totp_code_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_totp_code_desc",
|
||||
}
|
||||
],
|
||||
)
|
||||
),
|
||||
get_fields_from_field(
|
||||
input_widget(
|
||||
id="profile-totp-code",
|
||||
name="current_password",
|
||||
label="profile_current_password", # keep it (a18n)
|
||||
value="",
|
||||
isClipboard=True,
|
||||
pattern="", # add your pattern if needed
|
||||
columns={"pc": 12, "tablet": 12, "mobile": 12},
|
||||
placeholder="profile_current_password_placeholder", # keep it (a18n)
|
||||
popovers=[
|
||||
{
|
||||
"iconName": "info",
|
||||
"text": "profile_current_password_desc",
|
||||
}
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
buttons=[
|
||||
button_widget(
|
||||
id="profile-disable-submit",
|
||||
text="action_enable",
|
||||
iconName="plus",
|
||||
iconColor="check",
|
||||
color="success",
|
||||
size="normal",
|
||||
type="submit",
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def profile_tabs():
|
||||
return {
|
||||
"type": "tabs",
|
||||
"widgets": [
|
||||
button_group_widget(
|
||||
buttons=[
|
||||
button_widget(
|
||||
text="profile_tab_profile",
|
||||
display=["main", 0],
|
||||
size="tab",
|
||||
color="info",
|
||||
iconColor="white",
|
||||
iconName="list",
|
||||
),
|
||||
button_widget(
|
||||
text="profile_tab_account",
|
||||
color="edit",
|
||||
display=["main", 1],
|
||||
size="tab",
|
||||
iconColor="white",
|
||||
iconName="pen",
|
||||
),
|
||||
button_widget(
|
||||
text="profile_tab_totp",
|
||||
color="success",
|
||||
display=["main", 2],
|
||||
size="tab",
|
||||
iconColor="white",
|
||||
iconName="lock",
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def fallback_message(msg: str, display: Optional[list] = None) -> dict:
|
||||
|
||||
return {
|
||||
"type": "void",
|
||||
"display": display if display else [],
|
||||
"widgets": [
|
||||
unmatch_widget(text=msg),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def profile_builder(user: Optional[dict] = None) -> list:
|
||||
|
||||
if user is None or len(user) == 0:
|
||||
return [fallback_message("profile_user_not_found")]
|
||||
|
||||
totp_data = user.get("totp", None)
|
||||
|
||||
totp_form = None
|
||||
if totp_data is None or totp_data.get("is_totp", None) is None:
|
||||
totp_form = fallback_message("profile_totp_data_missing")
|
||||
|
||||
if not totp_data.get("is_totp"):
|
||||
totp_form = totp_disable_form(
|
||||
totp_img=totp_data.get("totp_image", ""),
|
||||
totp_secret=totp_data.get("totp_secret", ""),
|
||||
)
|
||||
|
||||
if totp_data.get("is_totp"):
|
||||
totp_form = totp_enable_form(
|
||||
totp_recovery_codes=totp_data.get("totp_recovery_codes", None),
|
||||
is_recovery_refreshed=totp_data.get("is_recovery_refreshed", False),
|
||||
)
|
||||
|
||||
totp_enable_form(
|
||||
totp_recovery_codes=user.get("totp_recovery_codes", None),
|
||||
is_recovery_refreshed=user.get("is_recovery_refreshed", False),
|
||||
)
|
||||
|
||||
return [
|
||||
# Tabs is button group with display value and a size tab inside a tabs container
|
||||
profile_tabs(),
|
||||
profile_info(user_profile=user.get("profile", None)),
|
||||
profile_account_form(email=user.get("email", "")),
|
||||
totp_form,
|
||||
]
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import json
|
||||
import base64
|
||||
|
||||
# TODO : REMOVE operation by custom endpoint
|
||||
|
||||
from builder.utils.widgets import button, button_group, title, text, tabulator, fields, upload, input, combobox, checkbox, select, editor
|
||||
|
||||
# Define data to put in profile widgets
|
||||
# - username
|
||||
# - email
|
||||
# - created_method
|
||||
# - is_superadmin
|
||||
# - role
|
||||
# - role_description
|
||||
# - permissions (liste of permissions [])
|
||||
# - creation_date
|
||||
# - last_update (last time update user info)
|
||||
profile_widgets = []
|
||||
|
||||
# Define data to put in profile form widgets
|
||||
# - update password
|
||||
# - update email
|
||||
account_widgets = []
|
||||
|
||||
# Define data to put in totp widgets
|
||||
# if want to enable totp (currently disabled):
|
||||
# text with state
|
||||
# show QRcode SVG
|
||||
# - form (endpoint /totp-enable) : totp secret (type password), totp code, password
|
||||
# Case currently enabled :
|
||||
# text with state
|
||||
# after first totp setup, show recovery codes
|
||||
# form refresh recovery codes button that will redisplay recovery (endpoint /totp-refresh) : password (warning that will remove previous)
|
||||
# form disabled (endpoint /totp-disable) : totp code || recovery code, password
|
||||
totp_widgets = []
|
||||
|
||||
builder = [
|
||||
{
|
||||
"type": "card",
|
||||
"display": ["main", 1],
|
||||
"widgets": [
|
||||
profile_widgets,
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"display": ["main", 2],
|
||||
"widgets": [
|
||||
account_widgets,
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"display": ["main", 3],
|
||||
"widgets": [
|
||||
totp_widgets,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
with open("profile2.json", "w") as f:
|
||||
f.write(json.dumps(builder))
|
||||
|
||||
output_base64_bytes = base64.b64encode(bytes(json.dumps(builder), "utf-8"))
|
||||
|
||||
output_base64_string = output_base64_bytes.decode("ascii")
|
||||
|
||||
|
||||
with open("profile2.txt", "w") as f:
|
||||
f.write(output_base64_string)
|
||||
40
src/ui/client/builder/test_profile.py
Normal file
40
src/ui/client/builder/test_profile.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from utils import save_builder
|
||||
|
||||
from pages.profile import profile_builder
|
||||
|
||||
# Define data to put in profile widgets
|
||||
# - username
|
||||
# - email
|
||||
# - created_method
|
||||
# - is_superadmin
|
||||
# - role
|
||||
# - role_description
|
||||
# - permissions (liste of permissions [])
|
||||
# - creation_date
|
||||
# - last_update (last time update user info)
|
||||
user = {
|
||||
"profile": [
|
||||
{
|
||||
"profile_username": "username",
|
||||
"profile_email": "email",
|
||||
"profile_created_method": "created_method",
|
||||
"role": "admin",
|
||||
"role_description": "role_description",
|
||||
"permissions": "read, write, admin",
|
||||
"creation_date": "date",
|
||||
"last_update": "date",
|
||||
}
|
||||
],
|
||||
"totp": {
|
||||
"is_totp": False,
|
||||
"totp_image": "", # base 64 that will be add in an img tag src
|
||||
"totp_recovery_codes": [{"0": "code_0"}, {"1": "code_1"}, {"2": "code_2"}],
|
||||
"is_recovery_refreshed": False,
|
||||
"totp_secret": "totp_secret",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
builder = profile_builder(user=user)
|
||||
|
||||
save_builder(page_name="profile", output=builder, script_name="profile")
|
||||
|
|
@ -33,6 +33,8 @@ import Text from "@components/Widget/Text.vue";
|
|||
* inpType: "input",
|
||||
* },
|
||||
* ],
|
||||
* @param {String} [title=""] - Form title
|
||||
* @param {String} [subtitle=""] - Form subtitle
|
||||
* @param {Object} fields - List of Fields component to display
|
||||
* @param {Object} buttons - We need to send a regular ButtonGroup buttons prop
|
||||
* @param {String} [containerClass=""] - Container additional class
|
||||
|
|
@ -44,6 +46,16 @@ import Text from "@components/Widget/Text.vue";
|
|||
|
||||
const props = defineProps({
|
||||
// id && value && method
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
|
@ -107,6 +119,8 @@ onMounted(() => {
|
|||
:containerClass="`form-regular-container`"
|
||||
>
|
||||
<div class="form-regular-wrap">
|
||||
<Title v-if="props.title" type="card" :title="props.title" />
|
||||
<Subtitle v-if="props.subtitle" type="card" :subtitle="props.subtitle" />
|
||||
<div
|
||||
:class="[
|
||||
'layout-settings-regular',
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@
|
|||
"action_toggle": "toggle {name}",
|
||||
"action_unban": "unban {name}",
|
||||
"action_entry": "entry {name}",
|
||||
"action_update": "update {name}",
|
||||
"action_back": "back",
|
||||
"home_version": "version",
|
||||
"home_all_features_available": "all features are available",
|
||||
|
|
|
|||
Loading…
Reference in a new issue