separate account settings from shop settings
All checks were successful
deploy / deploy (push) Successful in 3m28s
All checks were successful
deploy / deploy (push) Successful in 3m28s
- Create dedicated /admin/account page for user account management - Move email, password, and 2FA settings from /admin/settings - Add Account link to top of admin sidebar navigation - Add TOTP-based two-factor authentication with NimbleTOTP - Add TOTP verification LiveView for login flow - Add AccountController for TOTP session management - Remove Advanced section from settings (duplicated in dev tools) - Remove user email from sidebar footer (replaced by Account link) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1208,6 +1208,105 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-info-box .admin-btn-primary {
|
||||
color: var(--t-text-inverse);
|
||||
}
|
||||
|
||||
/* ── TOTP / 2FA ── */
|
||||
|
||||
.admin-totp-setup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-totp-qr {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-qr-code {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.admin-qr-code svg {
|
||||
display: block;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.admin-totp-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--admin-border);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-totp-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-copy-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--admin-bg-secondary);
|
||||
border-radius: 0.375rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.admin-copy-field code {
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.admin-backup-codes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-backup-code {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: white;
|
||||
border-radius: 0.25rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.admin-dl-compact {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.admin-dl-compact .admin-dl-row {
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Link variants ── */
|
||||
|
||||
.admin-link-danger {
|
||||
|
||||
@@ -242,6 +242,18 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-check {
|
||||
--hero-check: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="m4.5%2012.75%206%206%209-13.5"/></svg>');
|
||||
-webkit-mask: var(--hero-check);
|
||||
mask: var(--hero-check);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-check-badge {
|
||||
--hero-check-badge: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M9%2012.75%2011.25%2015%2015%209.75M21%2012c0%201.268-.63%202.39-1.593%203.068a3.745%203.745%200%200%201-1.043%203.296%203.745%203.745%200%200%201-3.296%201.043A3.745%203.745%200%200%201%2012%2021c-1.268%200-2.39-.63-3.068-1.593a3.746%203.746%200%200%201-3.296-1.043%203.745%203.745%200%200%201-1.043-3.296A3.745%203.745%200%200%201%203%2012c0-1.268.63-2.39%201.593-3.068a3.745%203.745%200%200%201%201.043-3.296%203.746%203.746%200%200%201%203.296-1.043A3.746%203.746%200%200%201%2012%203c1.268%200%202.39.63%203.068%201.593a3.746%203.746%200%200%201%203.296%201.043%203.746%203.746%200%200%201%201.043%203.296A3.745%203.745%200%200%201%2021%2012Z"/></svg>');
|
||||
-webkit-mask: var(--hero-check-badge);
|
||||
@@ -374,6 +386,18 @@
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-clipboard {
|
||||
--hero-clipboard: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M15.666%203.888A2.25%202.25%200%200%200%2013.5%202.25h-3c-1.03%200-1.9.693-2.166%201.638m7.332%200c.055.194.084.4.084.612v0a.75.75%200%200%201-.75.75H9a.75.75%200%200%201-.75-.75v0c0-.212.03-.418.084-.612m7.332%200c.646.049%201.288.11%201.927.184%201.1.128%201.907%201.077%201.907%202.185V19.5a2.25%202.25%200%200%201-2.25%202.25H6.75A2.25%202.25%200%200%201%204.5%2019.5V6.257c0-1.108.806-2.057%201.907-2.185a48.208%2048.208%200%200%201%201.927-.184"/></svg>');
|
||||
-webkit-mask: var(--hero-clipboard);
|
||||
mask: var(--hero-clipboard);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-clipboard-document {
|
||||
--hero-clipboard-document: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M8.25%207.5V6.108c0-1.135.845-2.098%201.976-2.192.373-.03.748-.057%201.123-.08M15.75%2018H18a2.25%202.25%200%200%200%202.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424%2048.424%200%200%200-1.123-.08M15.75%2018.75v-1.875a3.375%203.375%200%200%200-3.375-3.375h-1.5a1.125%201.125%200%200%201-1.125-1.125v-1.5A3.375%203.375%200%200%200%206.375%207.5H5.25m11.9-3.664A2.251%202.251%200%200%200%2015%202.25h-1.5a2.251%202.251%200%200%200-2.15%201.586m5.8%200c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75%207.5H4.875c-.621%200-1.125.504-1.125%201.125v12c0%20.621.504%201.125%201.125%201.125h9.75c.621%200%201.125-.504%201.125-1.125V16.5a9%209%200%200%200-9-9Z"/></svg>');
|
||||
-webkit-mask: var(--hero-clipboard-document);
|
||||
@@ -542,18 +566,6 @@
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-document-plus {
|
||||
--hero-document-plus: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M19.5%2014.25v-2.625a3.375%203.375%200%200%200-3.375-3.375h-1.5A1.125%201.125%200%200%201%2013.5%207.125v-1.5a3.375%203.375%200%200%200-3.375-3.375H8.25m3.75%209v6m3-3H9m1.5-12H5.625c-.621%200-1.125.504-1.125%201.125v17.25c0%20.621.504%201.125%201.125%201.125h12.75c.621%200%201.125-.504%201.125-1.125V11.25a9%209%200%200%200-9-9Z"/></svg>');
|
||||
-webkit-mask: var(--hero-document-plus);
|
||||
mask: var(--hero-document-plus);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-document-text {
|
||||
--hero-document-text: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M19.5%2014.25v-2.625a3.375%203.375%200%200%200-3.375-3.375h-1.5A1.125%201.125%200%200%201%2013.5%207.125v-1.5a3.375%203.375%200%200%200-3.375-3.375H8.25m0%2012.75h7.5m-7.5%203H12M10.5%202.25H5.625c-.621%200-1.125.504-1.125%201.125v17.25c0%20.621.504%201.125%201.125%201.125h12.75c.621%200%201.125-.504%201.125-1.125V11.25a9%209%200%200%200-9-9Z"/></svg>');
|
||||
-webkit-mask: var(--hero-document-text);
|
||||
@@ -1010,6 +1022,18 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-shield-check-mini {
|
||||
--hero-shield-check-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20fill-rule="evenodd"%20d="M9.661%202.237a.531.531%200%200%201%20.678%200%2011.947%2011.947%200%200%200%207.078%202.749.5.5%200%200%201%20.479.425c.069.52.104%201.05.104%201.59%200%205.162-3.26%209.563-7.834%2011.256a.48.48%200%200%201-.332%200C5.26%2016.564%202%2012.163%202%207c0-.538.035-1.069.104-1.589a.5.5%200%200%201%20.48-.425%2011.947%2011.947%200%200%200%207.077-2.75Zm4.196%205.954a.75.75%200%200%200-1.214-.882l-3.483%204.79-1.88-1.88a.75.75%200%201%200-1.06%201.061l2.5%202.5a.75.75%200%200%200%201.137-.089l4-5.5Z"%20clip-rule="evenodd"/></svg>');
|
||||
-webkit-mask: var(--hero-shield-check-mini);
|
||||
mask: var(--hero-shield-check-mini);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-shopping-bag {
|
||||
--hero-shopping-bag: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M15.75%2010.5V6a3.75%203.75%200%201%200-7.5%200v4.5m11.356-1.993%201.263%2012c.07.665-.45%201.243-1.119%201.243H4.25a1.125%201.125%200%200%201-1.12-1.243l1.264-12A1.125%201.125%200%200%201%205.513%207.5h12.974c.576%200%201.059.435%201.119%201.007ZM8.625%2010.5a.375.375%200%201%201-.75%200%20.375.375%200%200%201%20.75%200Zm7.5%200a.375.375%200%201%201-.75%200%20.375.375%200%200%201%20.75%200Z"/></svg>');
|
||||
-webkit-mask: var(--hero-shopping-bag);
|
||||
@@ -1034,18 +1058,6 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-signal {
|
||||
--hero-signal: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M9.348%2014.652a3.75%203.75%200%200%201%200-5.304m5.304%200a3.75%203.75%200%200%201%200%205.304m-7.425%202.121a6.75%206.75%200%200%201%200-9.546m9.546%200a6.75%206.75%200%200%201%200%209.546M5.106%2018.894c-3.808-3.807-3.808-9.98%200-13.788m13.788%200c3.808%203.807%203.808%209.98%200%2013.788M12%2012h.008v.008H12V12Zm.375%200a.375.375%200%201%201-.75%200%20.375.375%200%200%201%20.75%200Z"/></svg>');
|
||||
-webkit-mask: var(--hero-signal);
|
||||
mask: var(--hero-signal);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-squares-2x2 {
|
||||
--hero-squares-2x2: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M3.75%206A2.25%202.25%200%200%201%206%203.75h2.25A2.25%202.25%200%200%201%2010.5%206v2.25a2.25%202.25%200%200%201-2.25%202.25H6a2.25%202.25%200%200%201-2.25-2.25V6ZM3.75%2015.75A2.25%202.25%200%200%201%206%2013.5h2.25a2.25%202.25%200%200%201%202.25%202.25V18a2.25%202.25%200%200%201-2.25%202.25H6A2.25%202.25%200%200%201%203.75%2018v-2.25ZM13.5%206a2.25%202.25%200%200%201%202.25-2.25H18A2.25%202.25%200%200%201%2020.25%206v2.25A2.25%202.25%200%200%201%2018%2010.5h-2.25a2.25%202.25%200%200%201-2.25-2.25V6ZM13.5%2015.75a2.25%202.25%200%200%201%202.25-2.25H18a2.25%202.25%200%200%201%202.25%202.25V18A2.25%202.25%200%200%201%2018%2020.25h-2.25A2.25%202.25%200%200%201%2013.5%2018v-2.25Z"/></svg>');
|
||||
-webkit-mask: var(--hero-squares-2x2);
|
||||
@@ -1166,6 +1178,18 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-user-circle {
|
||||
--hero-user-circle: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M17.982%2018.725A7.488%207.488%200%200%200%2012%2015.75a7.488%207.488%200%200%200-5.982%202.975m11.963%200a9%209%200%201%200-11.963%200m11.963%200A8.966%208.966%200%200%201%2012%2021a8.966%208.966%200%200%201-5.982-2.275M15%209.75a3%203%200%201%201-6%200%203%203%200%200%201%206%200Z"/></svg>');
|
||||
-webkit-mask: var(--hero-user-circle);
|
||||
mask: var(--hero-user-circle);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-users {
|
||||
--hero-users: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="M15%2019.128a9.38%209.38%200%200%200%202.625.372%209.337%209.337%200%200%200%204.121-.952%204.125%204.125%200%200%200-7.533-2.493M15%2019.128v-.003c0-1.113-.285-2.16-.786-3.07M15%2019.128v.106A12.318%2012.318%200%200%201%208.624%2021c-2.331%200-4.512-.645-6.374-1.766l-.001-.109a6.375%206.375%200%200%201%2011.964-3.07M12%206.375a3.375%203.375%200%201%201-6.75%200%203.375%203.375%200%200%201%206.75%200Zm8.25%202.25a2.625%202.625%200%201%201-5.25%200%202.625%202.625%200%200%201%205.25%200Z"/></svg>');
|
||||
-webkit-mask: var(--hero-users);
|
||||
@@ -1190,18 +1214,6 @@
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-x-circle {
|
||||
--hero-x-circle: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="none"%20viewBox="0%200%2024%2024"%20stroke-width="1.5"%20stroke="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20stroke-linecap="round"%20stroke-linejoin="round"%20d="m9.75%209.75%204.5%204.5m0-4.5-4.5%204.5M21%2012a9%209%200%201%201-18%200%209%209%200%200%201%2018%200Z"/></svg>');
|
||||
-webkit-mask: var(--hero-x-circle);
|
||||
mask: var(--hero-x-circle);
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-x-circle-mini {
|
||||
--hero-x-circle-mini: url('data:image/svg+xml;utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2020%2020"%20fill="currentColor"%20aria-hidden="true"%20data-slot="icon">%20%20<path%20fill-rule="evenodd"%20d="M10%2018a8%208%200%201%200%200-16%208%208%200%200%200%200%2016ZM8.28%207.22a.75.75%200%200%200-1.06%201.06L8.94%2010l-1.72%201.72a.75.75%200%201%200%201.06%201.06L10%2011.06l1.72%201.72a.75.75%200%201%200%201.06-1.06L11.06%2010l1.72-1.72a.75.75%200%200%200-1.06-1.06L10%208.94%208.28%207.22Z"%20clip-rule="evenodd"/></svg>');
|
||||
-webkit-mask: var(--hero-x-circle-mini);
|
||||
|
||||
@@ -735,6 +735,46 @@ const EditorSheet = {
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard: copy text from a target element to clipboard
|
||||
const Clipboard = {
|
||||
mounted() {
|
||||
this.el.addEventListener("click", () => {
|
||||
const targetId = this.el.dataset.copyTarget
|
||||
const target = document.getElementById(targetId)
|
||||
if (!target) return
|
||||
|
||||
const text = target.textContent.trim()
|
||||
this._copyText(text).then(() => {
|
||||
const span = this.el.querySelector("span")
|
||||
if (span) {
|
||||
const original = span.textContent
|
||||
span.textContent = "Copied!"
|
||||
setTimeout(() => { span.textContent = original }, 1500)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
_copyText(text) {
|
||||
// Modern API (requires HTTPS or localhost)
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text)
|
||||
}
|
||||
// Fallback for HTTP contexts
|
||||
return new Promise((resolve) => {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = text
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand("copy")
|
||||
document.body.removeChild(textarea)
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DirtyGuard + Ctrl+Z / Ctrl+Shift+Z undo/redo for page editors
|
||||
const EditorKeyboard = {
|
||||
mounted() {
|
||||
@@ -788,7 +828,7 @@ const EditorKeyboard = {
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
params: {_csrf_token: csrfToken, screen_width: window.innerWidth},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, MobileNavDrawer, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard, EditorSheet},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, MobileNavDrawer, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, Clipboard, DirtyGuard, EditorKeyboard, EditorSheet},
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
|
||||
Reference in New Issue
Block a user