Development
This commit is contained in:
parent
c561f2f548
commit
1beb660be1
8 changed files with 280 additions and 48 deletions
|
|
@ -43,6 +43,7 @@ def init_accounts_db():
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
access_level INTEGER NOT NULL,
|
access_level INTEGER NOT NULL,
|
||||||
hashed_password TEXT,
|
hashed_password TEXT,
|
||||||
|
requested_email TEXT,
|
||||||
created_ts INTEGER NOT NULL,
|
created_ts INTEGER NOT NULL,
|
||||||
created_by TEXT NOT NULL
|
created_by TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
@ -68,6 +69,11 @@ def init_accounts_db():
|
||||||
);
|
);
|
||||||
''')
|
''')
|
||||||
con.commit()
|
con.commit()
|
||||||
|
try:
|
||||||
|
con.execute('ALTER TABLE accounts ADD COLUMN requested_email TEXT')
|
||||||
|
con.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
_LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'}
|
_LEVEL_INT_TO_STR = {0: 'nothing', 1: 'viewer', 2: 'administrator', 3: 'manager'}
|
||||||
|
|
@ -83,6 +89,7 @@ def _account_row_to_dict(row):
|
||||||
d['access_level_int'] = d.get('access_level', 1)
|
d['access_level_int'] = d.get('access_level', 1)
|
||||||
d['access_level'] = _LEVEL_INT_TO_STR.get(d['access_level_int'], 'viewer')
|
d['access_level'] = _LEVEL_INT_TO_STR.get(d['access_level_int'], 'viewer')
|
||||||
d['account_status'] = 'active' if d.get('hashed_password') else 'pending'
|
d['account_status'] = 'active' if d.get('hashed_password') else 'pending'
|
||||||
|
d['requested_email'] = d.get('requested_email') or ''
|
||||||
d['account_created_by'] = d.get('created_by', '')
|
d['account_created_by'] = d.get('created_by', '')
|
||||||
ts = d.get('created_ts', 0)
|
ts = d.get('created_ts', 0)
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,33 @@ def form_login():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
if not account.get('hashed_password'):
|
if not account.get('hashed_password'):
|
||||||
|
# Either brand-new account or email change approved - check for a pending verification row
|
||||||
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
client = con.execute(
|
||||||
|
'SELECT * FROM clients WHERE email=? AND verification_code IS NOT NULL', (email,)
|
||||||
|
).fetchone()
|
||||||
|
if client and client['cookie_unique_token'] != session.sid:
|
||||||
|
con.execute(
|
||||||
|
'''INSERT INTO clients (cookie_unique_token, email, hashed_password, verification_code, code_sent_ts)
|
||||||
|
VALUES (?,?,?,?,?)
|
||||||
|
ON CONFLICT(cookie_unique_token) DO UPDATE SET
|
||||||
|
email=excluded.email,
|
||||||
|
hashed_password=excluded.hashed_password,
|
||||||
|
verification_code=excluded.verification_code,
|
||||||
|
code_sent_ts=excluded.code_sent_ts''',
|
||||||
|
(session.sid, client['email'], client['hashed_password'],
|
||||||
|
client['verification_code'], client['code_sent_ts'])
|
||||||
|
)
|
||||||
|
con.execute('DELETE FROM clients WHERE cookie_unique_token=?',
|
||||||
|
(client['cookie_unique_token'],))
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
except Exception:
|
||||||
|
client = None
|
||||||
|
if client:
|
||||||
|
flash('Please check your inbox and enter your verification code.', 'info')
|
||||||
|
return redirect('/accountverifyemail')
|
||||||
flash('Account setup is not complete. Please use Create Account to set your password first.', 'error')
|
flash('Account setup is not complete. Please use Create Account to set your password first.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Blueprint, request, session, redirect, flash
|
from flask import Blueprint, request, session, redirect, flash
|
||||||
import os, re, sqlite3
|
import os, re, secrets, sqlite3, time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import auth
|
import auth
|
||||||
import config_utils
|
import config_utils
|
||||||
|
|
@ -13,6 +13,93 @@ bp = Blueprint(_PAGE, __name__)
|
||||||
VALID_LEVELS = {'viewer': 1, 'administrator': 2, 'manager': 3}
|
VALID_LEVELS = {'viewer': 1, 'administrator': 2, 'manager': 3}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/accountmanage/email_change_deny', methods=['POST'])
|
||||||
|
@auth.require_level('manager')
|
||||||
|
def email_change_deny():
|
||||||
|
account_id = request.form.get('account_id', '').strip()
|
||||||
|
if not account_id:
|
||||||
|
flash('Invalid request.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
con.execute('UPDATE accounts SET requested_email=NULL WHERE account_id=?', (account_id,))
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
flash('Email change request denied.', 'success')
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not deny request: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/accountmanage/email_change_approve', methods=['POST'])
|
||||||
|
@auth.require_level('manager')
|
||||||
|
def email_change_approve():
|
||||||
|
from pages.accountcreate.action import _send_verification_email, CODE_TTL_SECS
|
||||||
|
|
||||||
|
account_id = request.form.get('account_id', '').strip()
|
||||||
|
if not account_id:
|
||||||
|
flash('Invalid request.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
row = con.execute(
|
||||||
|
'SELECT * FROM accounts WHERE account_id=?', (account_id,)
|
||||||
|
).fetchone()
|
||||||
|
con.close()
|
||||||
|
except Exception:
|
||||||
|
row = None
|
||||||
|
|
||||||
|
if not row or not row['requested_email']:
|
||||||
|
flash('No pending email change found.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
new_email = row['requested_email']
|
||||||
|
old_password = row['hashed_password']
|
||||||
|
code = f'{secrets.randbelow(1000000):06d}'
|
||||||
|
code_sent_ts = int(time.time())
|
||||||
|
|
||||||
|
try:
|
||||||
|
_send_verification_email(new_email, code)
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not send verification email: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
last_cookie = con.execute(
|
||||||
|
'SELECT session_id FROM sessions WHERE account_id=? ORDER BY last_seen_ts DESC LIMIT 1',
|
||||||
|
(account_id,)
|
||||||
|
).fetchone()
|
||||||
|
cookie = last_cookie['session_id'] if last_cookie else None
|
||||||
|
|
||||||
|
if cookie:
|
||||||
|
con.execute(
|
||||||
|
'''INSERT INTO clients (cookie_unique_token, email, hashed_password, verification_code, code_sent_ts)
|
||||||
|
VALUES (?,?,?,?,?)
|
||||||
|
ON CONFLICT(cookie_unique_token) DO UPDATE SET
|
||||||
|
email=excluded.email,
|
||||||
|
hashed_password=excluded.hashed_password,
|
||||||
|
verification_code=excluded.verification_code,
|
||||||
|
code_sent_ts=excluded.code_sent_ts''',
|
||||||
|
(cookie, new_email, old_password, code, code_sent_ts)
|
||||||
|
)
|
||||||
|
|
||||||
|
con.execute(
|
||||||
|
'UPDATE accounts SET email=?, hashed_password=NULL, requested_email=NULL WHERE account_id=?',
|
||||||
|
(new_email, account_id)
|
||||||
|
)
|
||||||
|
con.execute('DELETE FROM sessions WHERE account_id=?', (account_id,))
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not apply email change: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
flash(f'Email changed to {new_email}. A verification email has been sent.', 'success')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/accountmanage/session_invalidate', methods=['POST'])
|
@bp.route('/action/accountmanage/session_invalidate', methods=['POST'])
|
||||||
@auth.require_level('manager')
|
@auth.require_level('manager')
|
||||||
def session_invalidate():
|
def session_invalidate():
|
||||||
|
|
|
||||||
|
|
@ -25,34 +25,8 @@
|
||||||
"label": "User Accounts",
|
"label": "User Accounts",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "raw_html",
|
||||||
"datasource": "config:accounts",
|
"html": "%ACCOUNTS_TABLE%"
|
||||||
"empty_message": "No accounts configured.",
|
|
||||||
"columns": [
|
|
||||||
{"label": "Email Address", "field": "email_address"},
|
|
||||||
{"label": "Access Level", "field": "access_level"},
|
|
||||||
{"label": "Added By", "field": "account_created_by"},
|
|
||||||
{"label": "Added", "field": "account_created_ts"},
|
|
||||||
{
|
|
||||||
"label": "Status",
|
|
||||||
"field": "account_status",
|
|
||||||
"render": "badge_active_inactive"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"row_actions": [
|
|
||||||
{
|
|
||||||
"method": "js_edit",
|
|
||||||
"target": "edit-form",
|
|
||||||
"text": "Edit",
|
|
||||||
"class": "btn-ghost btn-sm"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "/action/accountmanage/accounts_delete",
|
|
||||||
"method": "post",
|
|
||||||
"text": "Delete",
|
|
||||||
"class": "btn-danger btn-sm"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,76 @@ def _active_sessions_table():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _accounts_table():
|
||||||
|
accounts = config_utils.list_accounts()
|
||||||
|
if not accounts:
|
||||||
|
return '<p class="text-muted" style="margin:0">No accounts.</p>'
|
||||||
|
|
||||||
|
trs = ''
|
||||||
|
for i, acct in enumerate(accounts):
|
||||||
|
email = acct.get('email_address', '')
|
||||||
|
req_email = acct.get('requested_email', '')
|
||||||
|
acct_id = acct.get('account_id', '')
|
||||||
|
level = acct.get('access_level', 'viewer')
|
||||||
|
status = acct.get('account_status', 'pending')
|
||||||
|
|
||||||
|
if req_email:
|
||||||
|
approve_btn = (
|
||||||
|
f'<form method="post" action="/action/accountmanage/email_change_approve"'
|
||||||
|
f' style="display:inline;margin:0">'
|
||||||
|
f'<input type="hidden" name="account_id" value="{factory.e(acct_id)}">'
|
||||||
|
f'<button type="submit" class="btn-link">Approve</button>'
|
||||||
|
f'</form>'
|
||||||
|
)
|
||||||
|
deny_btn = (
|
||||||
|
f'<form method="post" action="/action/accountmanage/email_change_deny"'
|
||||||
|
f' style="display:inline;margin:0">'
|
||||||
|
f'<input type="hidden" name="account_id" value="{factory.e(acct_id)}">'
|
||||||
|
f'<button type="submit" class="btn-link btn-link-danger">Deny</button>'
|
||||||
|
f'</form>'
|
||||||
|
)
|
||||||
|
email_cell = (
|
||||||
|
f'{factory.e(email)}<br>'
|
||||||
|
f'<span class="text-muted" style="font-size:0.85em">'
|
||||||
|
f'{approve_btn} or {deny_btn} change to: {factory.e(req_email)}'
|
||||||
|
f'</span>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
email_cell = factory.e(email)
|
||||||
|
|
||||||
|
row_json = factory.e(json.dumps({'email_address': email, 'access_level': level}))
|
||||||
|
edit_btn = (
|
||||||
|
f'<button type="button" class="btn btn-ghost btn-sm row-edit-btn"'
|
||||||
|
f' data-row-index="{i}" data-row="{row_json}"'
|
||||||
|
f' data-target="edit-form">Edit</button>'
|
||||||
|
)
|
||||||
|
delete_btn = (
|
||||||
|
f'<form method="post" action="/action/accountmanage/accounts_delete"'
|
||||||
|
f' class="form-inline">'
|
||||||
|
f'<input type="hidden" name="row_index" value="{i}">'
|
||||||
|
f'<button type="submit" class="btn btn-danger btn-sm">Delete</button>'
|
||||||
|
f'</form>'
|
||||||
|
)
|
||||||
|
|
||||||
|
trs += (
|
||||||
|
f'<tr>'
|
||||||
|
f'<td class="table-cell">{email_cell}</td>'
|
||||||
|
f'<td class="table-cell">{factory.e(level)}</td>'
|
||||||
|
f'<td class="table-cell">{factory.e(status)}</td>'
|
||||||
|
f'<td class="col-actions">{edit_btn} {delete_btn}</td>'
|
||||||
|
f'</tr>'
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
'<table class="data-table"><thead><tr>'
|
||||||
|
'<th class="table-header">Email</th>'
|
||||||
|
'<th class="table-header">Access Level</th>'
|
||||||
|
'<th class="table-header">Status</th>'
|
||||||
|
'<th class="table-header"></th>'
|
||||||
|
'</tr></thead><tbody>' + trs + '</tbody></table>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def collect_tokens(cfg):
|
def collect_tokens(cfg):
|
||||||
tokens = config_utils.collect_layout_tokens(cfg)
|
tokens = config_utils.collect_layout_tokens(cfg)
|
||||||
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
|
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
|
||||||
|
|
@ -89,6 +159,7 @@ def collect_tokens(cfg):
|
||||||
{'value': 'manager', 'label': 'Manager (full access including account management)'},
|
{'value': 'manager', 'label': 'Manager (full access including account management)'},
|
||||||
])
|
])
|
||||||
tokens['ACTIVE_SESSIONS_TABLE'] = _active_sessions_table()
|
tokens['ACTIVE_SESSIONS_TABLE'] = _active_sessions_table()
|
||||||
|
tokens['ACCOUNTS_TABLE'] = _accounts_table()
|
||||||
content = factory.load_json(f'{factory.PAGES_DIR}/accountmanage/content.json')
|
content = factory.load_json(f'{factory.PAGES_DIR}/accountmanage/content.json')
|
||||||
for table_item in factory.iter_table_items(content.get('items', [])):
|
for table_item in factory.iter_table_items(content.get('items', [])):
|
||||||
ds = table_item.get('datasource', '')
|
ds = table_item.get('datasource', '')
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,39 @@ def accountdetails_save():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/preferences/email_change_request', methods=['POST'])
|
||||||
|
@auth.require_level('viewer')
|
||||||
|
def email_change_request():
|
||||||
|
new_email = sanitize.email(request.form.get('new_email', '').strip())
|
||||||
|
if not new_email:
|
||||||
|
flash('A valid email address is required.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
current_email = session.get('email_address', '').lower()
|
||||||
|
if new_email == current_email:
|
||||||
|
flash('That is already your current email address.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
if config_utils.get_account_by_email(new_email):
|
||||||
|
flash('That email address is already in use.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
con = config_utils.open_accounts_db()
|
||||||
|
con.execute(
|
||||||
|
'UPDATE accounts SET requested_email=? WHERE account_id=?',
|
||||||
|
(new_email, session.get('account_id', ''))
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f'Could not submit request: {exc}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
flash('Email change request submitted. A manager will review it.', 'success')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/preferences/changepassword_save', methods=['POST'])
|
@bp.route('/action/preferences/changepassword_save', methods=['POST'])
|
||||||
@auth.require_level('viewer')
|
@auth.require_level('viewer')
|
||||||
def changepassword_save():
|
def changepassword_save():
|
||||||
|
|
|
||||||
|
|
@ -23,31 +23,19 @@
|
||||||
"action": "/action/preferences/accountdetails_save",
|
"action": "/action/preferences/accountdetails_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
|
||||||
"type": "field",
|
|
||||||
"label": "Email Address",
|
|
||||||
"name": "email",
|
|
||||||
"input_type": "text",
|
|
||||||
"value": "%PREF_EMAIL%",
|
|
||||||
"readonly": true,
|
|
||||||
"hint": "Contact your manager to change your email address."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field",
|
||||||
"label": "Timezone",
|
"label": "Timezone",
|
||||||
"name": "timezone",
|
"name": "timezone",
|
||||||
"input_type": "select",
|
"input_type": "select",
|
||||||
"value": "%PREF_TIMEZONE%",
|
"value": "%PREF_TIMEZONE%",
|
||||||
"options": "%TIMEZONE_OPTIONS%",
|
"options": "%TIMEZONE_OPTIONS%"
|
||||||
"hint": "All timestamps will be displayed in this timezone."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "button_row",
|
"type": "button_row",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/preferences/accountdetails_save",
|
|
||||||
"method": "post",
|
|
||||||
"text": "Save Preferences"
|
"text": "Save Preferences"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -56,6 +44,40 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Change Email",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%PENDING_EMAIL_BAR%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/preferences/email_change_request",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Email Address",
|
||||||
|
"name": "new_email",
|
||||||
|
"input_type": "text",
|
||||||
|
"value": "%PREF_EMAIL%",
|
||||||
|
"placeholder": "New email address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"text": "Submit Request"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "card",
|
"type": "card",
|
||||||
"label": "Change Password",
|
"label": "Change Password",
|
||||||
|
|
@ -91,8 +113,6 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/preferences/changepassword_save",
|
|
||||||
"method": "post",
|
|
||||||
"text": "Change Password"
|
"text": "Change Password"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,25 @@ import json
|
||||||
from flask import session
|
from flask import session
|
||||||
import sanitize
|
import sanitize
|
||||||
import config_utils
|
import config_utils
|
||||||
|
import factory
|
||||||
|
|
||||||
|
|
||||||
def collect_tokens(cfg):
|
def collect_tokens(cfg):
|
||||||
tokens = config_utils.collect_layout_tokens(cfg)
|
tokens = config_utils.collect_layout_tokens(cfg)
|
||||||
blank = [{'value': '', 'label': '-- Select Timezone --'}]
|
blank = [{'value': '', 'label': '-- Select Timezone --'}]
|
||||||
tokens['PREF_EMAIL'] = session.get('email_address', '')
|
tokens['PREF_EMAIL'] = session.get('email_address', '')
|
||||||
tokens['PREF_TIMEZONE'] = session.get('timezone', '')
|
tokens['PREF_TIMEZONE'] = session.get('timezone', '')
|
||||||
tokens['TIMEZONE_OPTIONS'] = json.dumps(blank + [{'value': tz, 'label': tz} for tz in sanitize.VALID_TIMEZONES])
|
tokens['TIMEZONE_OPTIONS'] = json.dumps(blank + [{'value': tz, 'label': tz} for tz in sanitize.VALID_TIMEZONES])
|
||||||
|
|
||||||
|
account = config_utils.get_account_by_id(session.get('account_id', ''))
|
||||||
|
requested = (account or {}).get('requested_email', '')
|
||||||
|
if requested:
|
||||||
|
tokens['PENDING_EMAIL_BAR'] = (
|
||||||
|
f'<div class="info-bar info-bar-inline info-bar-warning">'
|
||||||
|
f'A request to change email to {factory.e(requested)} is pending approval.'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tokens['PENDING_EMAIL_BAR'] = ''
|
||||||
|
|
||||||
return tokens
|
return tokens
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue