Rename Suspend to Suspend access and Make user to Change role.
Assistant: grok Assistant-Session: 01a0d25d-d358-7e13-b84a-d007fbb7e34f
This commit is contained in:
parent
8f7cf0c69d
commit
14aee356cf
2 changed files with 46 additions and 11 deletions
|
|
@ -2071,10 +2071,18 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
||||||
def _invitation_admin_row(self, tenant: str, invitation: Any, csrf_token: str) -> str:
|
def _invitation_admin_row(self, tenant: str, invitation: Any, csrf_token: str) -> str:
|
||||||
actions = ""
|
actions = ""
|
||||||
if invitation.status.value == "pending":
|
if invitation.status.value == "pending":
|
||||||
actions = f"""<form method="post" action="/admin/{escape(tenant)}/invitations/{escape(invitation.invitation_id)}/resend">
|
invite_root = f"/admin/{escape(tenant)}/invitations/{escape(invitation.invitation_id)}"
|
||||||
<input type="hidden" name="csrf_token" value="{escape(csrf_token)}"><input type="hidden" name="version" value="{invitation.version}"><button type="submit">Resend</button></form>
|
hidden = f'<input type="hidden" name="csrf_token" value="{escape(csrf_token)}"><input type="hidden" name="version" value="{invitation.version}">'
|
||||||
<form method="post" action="/admin/{escape(tenant)}/invitations/{escape(invitation.invitation_id)}/expire">
|
actions = (
|
||||||
<input type="hidden" name="csrf_token" value="{escape(csrf_token)}"><input type="hidden" name="version" value="{invitation.version}"><button type="submit">Expire</button></form>"""
|
f'<form method="post" action="{invite_root}/resend">{hidden}'
|
||||||
|
f'<button type="submit">Resend</button>'
|
||||||
|
f'<button type="button" class="help" popovertarget="help-resend-{escape(invitation.invitation_id)}" aria-label="Explain Resend">?</button></form>'
|
||||||
|
f'<div id="help-resend-{escape(invitation.invitation_id)}" popover><p>Sends this invitation again.</p></div>'
|
||||||
|
f'<form method="post" action="{invite_root}/expire">{hidden}'
|
||||||
|
f'<button type="submit">Expire</button>'
|
||||||
|
f'<button type="button" class="help" popovertarget="help-expire-{escape(invitation.invitation_id)}" aria-label="Explain Expire">?</button></form>'
|
||||||
|
f'<div id="help-expire-{escape(invitation.invitation_id)}" popover><p>Stops this invitation. The person can no longer accept it.</p></div>'
|
||||||
|
)
|
||||||
expires = invitation.expires_at.isoformat() if invitation.expires_at else "—"
|
expires = invitation.expires_at.isoformat() if invitation.expires_at else "—"
|
||||||
events = [e for e in self.service.store.outbox_history() if e.tenant == tenant
|
events = [e for e in self.service.store.outbox_history() if e.tenant == tenant
|
||||||
and e.aggregate_id == invitation.invitation_id and e.event_type in {"family_invitation.created", "family_invitation.resent", "family_member.invited"}]
|
and e.aggregate_id == invitation.invitation_id and e.event_type in {"family_invitation.created", "family_invitation.resent", "family_member.invited"}]
|
||||||
|
|
@ -2092,9 +2100,16 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
||||||
status = account.status if account else AccountStatus.INVITED
|
status = account.status if account else AccountStatus.INVITED
|
||||||
inactive = status in {AccountStatus.SUSPENDED, AccountStatus.DISABLED}
|
inactive = status in {AccountStatus.SUSPENDED, AccountStatus.DISABLED}
|
||||||
root = f"/admin/{quote(tenant, safe='')}/users/{quote(membership.user_id, safe='')}"
|
root = f"/admin/{quote(tenant, safe='')}/users/{quote(membership.user_id, safe='')}"
|
||||||
def form(action: str, label: str, **fields: str) -> str:
|
def form(action: str, label: str, help_text: str, **fields: str) -> str:
|
||||||
hidden = "".join(f'<input type="hidden" name="{escape(k)}" value="{escape(v)}">' for k,v in fields.items())
|
hidden = "".join(f'<input type="hidden" name="{escape(k)}" value="{escape(v)}">' for k,v in fields.items())
|
||||||
return f'<form method="post" action="{root}/{action}"><input type="hidden" name="csrf_token" value="{escape(csrf_token)}">{hidden}<button type="submit">{label}</button></form>'
|
help_id = f"help-{action}-{membership.user_id}"
|
||||||
|
return (
|
||||||
|
f'<form method="post" action="{root}/{action}">'
|
||||||
|
f'<input type="hidden" name="csrf_token" value="{escape(csrf_token)}">{hidden}'
|
||||||
|
f'<button type="submit">{escape(label)}</button>'
|
||||||
|
f'<button type="button" class="help" popovertarget="{escape(help_id)}" aria-label="Explain {escape(label)}">?</button></form>'
|
||||||
|
f'<div id="{escape(help_id)}" popover><p>{escape(help_text)}</p></div>'
|
||||||
|
)
|
||||||
login = (f'<p>Login name: <strong>{escape(self._directory_login(directory.subject))}</strong></p>'
|
login = (f'<p>Login name: <strong>{escape(self._directory_login(directory.subject))}</strong></p>'
|
||||||
'<p>Pass this name on with the sign-in address. It is not the email address, '
|
'<p>Pass this name on with the sign-in address. It is not the email address, '
|
||||||
'and signing in by email does not work.</p>'
|
'and signing in by email does not work.</p>'
|
||||||
|
|
@ -2102,13 +2117,23 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
||||||
'<p>Password and authenticator status are not available here.</p>') if directory else '<p>Login not created. Prepare a login before asking this person to sign in.</p>'
|
'<p>Password and authenticator status are not available here.</p>') if directory else '<p>Login not created. Prepare a login before asking this person to sign in.</p>'
|
||||||
actions = ""
|
actions = ""
|
||||||
if not inactive:
|
if not inactive:
|
||||||
actions += form("provision", "Create password setup link" if directory else "Create login")
|
if directory:
|
||||||
actions += form("status", "Reactivate" if inactive else "Suspend", status="active" if inactive else "suspended")
|
actions += form("provision", "Create password setup link", "Makes a new single-use link so this person can set a password for the existing login. It does not change the login name.")
|
||||||
|
else:
|
||||||
|
actions += form("provision", "Create login", "Creates the directory login. The login name comes from the email name. It is not the display name, and this page cannot rename it afterwards.")
|
||||||
|
if inactive:
|
||||||
|
actions += form("status", "Reactivate", "Lets this person use this tenant again. It does not change the login name or the password.", status="active")
|
||||||
|
else:
|
||||||
|
actions += form("status", "Suspend access", "Stops this person using this tenant. The login and any other tenant stay as they are.", status="suspended")
|
||||||
if status != AccountStatus.DISABLED:
|
if status != AccountStatus.DISABLED:
|
||||||
actions += form("remove", "Remove account")
|
actions += form("remove", "Remove account", "Disables this person's account for this tenant. The shared login is kept.")
|
||||||
next_role = "user" if membership.kind == "tenant-admin" else "tenant-admin"
|
next_role = "user" if membership.kind == "tenant-admin" else "tenant-admin"
|
||||||
actions += form("role", "Make user" if next_role == "user" else "Make tenant administrator", role=next_role)
|
if next_role == "user":
|
||||||
if platform_operator: actions += form("recover", "Restore tenant account")
|
actions += form("role", "Change role", "This person administers the tenant. This changes them to an ordinary user of this tenant. It does not change the login name.", role=next_role)
|
||||||
|
else:
|
||||||
|
actions += form("role", "Make tenant administrator", "This person is an ordinary user. This lets them manage the users of this tenant.", role=next_role)
|
||||||
|
if platform_operator:
|
||||||
|
actions += form("recover", "Restore tenant account", "Turns this tenant account back on. It creates a directory login only when one is missing. It does not reset a password, remove an authenticator, or change the login name.")
|
||||||
return (f'<tr><td>{escape(user.display_name or membership.user_id) if user else escape(membership.user_id)}</td>'
|
return (f'<tr><td>{escape(user.display_name or membership.user_id) if user else escape(membership.user_id)}</td>'
|
||||||
f'<td>{escape(user.primary_email or "") if user else ""}</td><td>{escape(membership.kind)}</td>'
|
f'<td>{escape(user.primary_email or "") if user else ""}</td><td>{escape(membership.kind)}</td>'
|
||||||
f'<td>{escape(status.value)} for this tenant</td><td>{login}</td><td>{actions}</td></tr>')
|
f'<td>{escape(status.value)} for this tenant</td><td>{login}</td><td>{actions}</td></tr>')
|
||||||
|
|
@ -2489,9 +2514,12 @@ h1{{font:clamp(2.2rem,7vw,5.5rem)/.98 Georgia,serif;max-width:13ch}}a{{color:var
|
||||||
table{{width:100%;border-collapse:collapse;background:#fff}}th,td{{padding:.75rem;text-align:left;border-bottom:1px solid var(--line)}}
|
table{{width:100%;border-collapse:collapse;background:#fff}}th,td{{padding:.75rem;text-align:left;border-bottom:1px solid var(--line)}}
|
||||||
section{{margin:2rem 0}}form{{display:grid;gap:.8rem;max-width:42rem}}label{{display:grid;gap:.25rem}}
|
section{{margin:2rem 0}}form{{display:grid;gap:.8rem;max-width:42rem}}label{{display:grid;gap:.25rem}}
|
||||||
label:has(input[type=checkbox]){{display:flex;align-items:center;gap:.6rem}}
|
label:has(input[type=checkbox]){{display:flex;align-items:center;gap:.6rem}}
|
||||||
|
td form{{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;max-width:none}}
|
||||||
input,select,button{{font:inherit;padding:.65rem}}
|
input,select,button{{font:inherit;padding:.65rem}}
|
||||||
input[type=checkbox]{{width:1.15rem;height:1.15rem;margin:0;padding:0;flex:none}}
|
input[type=checkbox]{{width:1.15rem;height:1.15rem;margin:0;padding:0;flex:none}}
|
||||||
button{{background:var(--accent);color:white;border:0;border-radius:.3rem;cursor:pointer}}
|
button{{background:var(--accent);color:white;border:0;border-radius:.3rem;cursor:pointer}}
|
||||||
|
button.help{{background:transparent;color:var(--accent);border:1px solid var(--line);padding:.15rem .45rem;min-width:1.8rem}}
|
||||||
|
[popover]{{border:1px solid var(--line);background:#fff;color:var(--ink);padding:1rem;max-width:24rem;border-radius:.35rem}}
|
||||||
a:focus-visible,input:focus-visible,select:focus-visible,button:focus-visible{{outline:3px solid #e59f24;outline-offset:3px}}@media(max-width:640px){{body{{font-size:16px}}table{{display:block;overflow-x:auto}}}}
|
a:focus-visible,input:focus-visible,select:focus-visible,button:focus-visible{{outline:3px solid #e59f24;outline-offset:3px}}@media(max-width:640px){{body{{font-size:16px}}table{{display:block;overflow-x:auto}}}}
|
||||||
</style></head><body><header><strong>NetKingdom Identity</strong>{_ACCOUNT_NAVIGATION.get()}</header><main>{body}</main></body></html>"""
|
</style></head><body><header><strong>NetKingdom Identity</strong>{_ACCOUNT_NAVIGATION.get()}</header><main>{body}</main></body></html>"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -979,6 +979,13 @@ class PortalApplicationTests(unittest.TestCase):
|
||||||
self.assertEqual("200 OK", admin_page["status"])
|
self.assertEqual("200 OK", admin_page["status"])
|
||||||
self.assertIn(b"Lifecycle diagnostics", html)
|
self.assertIn(b"Lifecycle diagnostics", html)
|
||||||
self.assertIn(b"Restore tenant account", html)
|
self.assertIn(b"Restore tenant account", html)
|
||||||
|
self.assertIn(b">Suspend access</button>", html)
|
||||||
|
self.assertIn(b">Change role</button>", html)
|
||||||
|
self.assertNotIn(b">Make user</button>", html)
|
||||||
|
self.assertNotIn(b">Suspend</button>", html)
|
||||||
|
self.assertIn(b'popovertarget="help-role-', html)
|
||||||
|
self.assertIn(b"This person administers the tenant.", html)
|
||||||
|
self.assertIn(b"It does not change the login name.", html)
|
||||||
recovered, _ = invoke_confirmed(
|
recovered, _ = invoke_confirmed(
|
||||||
self.app,
|
self.app,
|
||||||
f"/admin/tenant:friendly:browser/users/{memberships[0].user_id}/recover",
|
f"/admin/tenant:friendly:browser/users/{memberships[0].user_id}/recover",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue