Update the site icon and wire the UI to email login
Icon: the supplied artwork is now UmbraTrans_Icon2_899x920.jpg, gold and
cyan on white, with the wordmark corrected to "UmbraTrans". It is copied
verbatim to web/assets/umbratrans-icon.jpg as the source, and
web/assets/umbratrans-emblem.png is derived from it and is what the pages
load: the book cropped to its ink bounds, the white page made transparent
by flood-filling inward from the corners so the white lettering inside
the book survives, letterboxed square rather than side-trimmed so no part
of the emblem is cut, and emitted at 256px.
A square crop of a 827x695 emblem has to choose between trimming the
sides and letterboxing, and trimming was cutting the book's frame. The
glow also moved from box-shadow to filter, because a box-shadow follows
the border box rather than the artwork's alpha and would have drawn a
glowing rectangle behind a book-shaped emblem.
The UI was still written against username login, so the sign-in form
could not have worked against the API: it posted {username, password} and
LoginRequest requires {email, password}. Fixed, along with the rest of
the email-verification flow that had no UI at all:
- the sign-in form collects an email address, and its copy says so;
- a 403 email_not_verified, which is only ever reached once the password
is already proven correct, reveals a panel offering a new link. The
resend wording is the server's and is identical for real and unknown
addresses;
- a confirmation view accepts a pasted link or token, and one is opened
automatically when the URL carries a token, which is then stripped from
the address bar so it is not left in history;
- the password-policy hint read the sign-in field to tell the user their
password must avoid their "account name", but the rule checks the
account's username. It now uses the signed-in account's handle, and
says nothing rather than the wrong thing when there is no handle;
- the provision form called email optional while the API requires it,
and let a member be created with no organization, which the store
rejects. Both are caught client-side now with wording that names the
field to fix;
- `serve` accepts --repository postgres, so the app can be exercised
against the authoritative store.
Verified against a live server on PostgreSQL: sign-in with the email
address, forced first-password change, provisioning an invited member,
401 for a wrong password and 403 email_not_verified for the right one,
and byte-identical resend answers for a real and an unknown address.
This commit is contained in:
@@ -30,7 +30,8 @@ The control surface references the existing Umbra Audio Review UI at `umbraudio.
|
||||
- warning: `#f59e0b`
|
||||
- danger: `#ef4444`
|
||||
- compact cards, top navigation, status badges, and data tables
|
||||
- supplied site icon: `/home/umbra/Documents/UmberTrans_Icon.jpg`, copied to the prototype as `web/assets/umbratrans-icon.jpg`
|
||||
- supplied site icon: `/home/umbra/Documents/UmbraTrans_Icon2_899x920.jpg` (899x920, gold and cyan, wordmark corrected to "UmbraTrans"), copied verbatim to `web/assets/umbratrans-icon.jpg` as the source artwork
|
||||
- `web/assets/umbratrans-emblem.png` is derived from it and is what the pages actually load: the book cropped to its ink bounds (x 47-873, y 81-776, the 776-796 gap being the separation from the wordmark), the white page made transparent by flood-filling inward from the corners so the white lettering inside the book survives, letterboxed square rather than side-trimmed so no part of the emblem is cut, and emitted at 256px. The favicon and every `.brand-image` reference it.
|
||||
|
||||
The workspace uses a deliberately different **Studio** language: a slate/indigo canvas, left navigation rail, teal action accent, amber review accent, larger reading areas, and a pipeline-oriented layout. It should feel like a production desk rather than an admin console.
|
||||
|
||||
|
||||
@@ -314,7 +314,15 @@ def serve(
|
||||
)
|
||||
parser.add_argument("--host", default="127.0.0.1", help="bind address (default: loopback)")
|
||||
parser.add_argument("--port", type=int, default=8000, help="bind port (default: 8000)")
|
||||
parser.add_argument("--repository", default="memory", choices=["memory", "json"], help="development repository")
|
||||
parser.add_argument(
|
||||
"--repository",
|
||||
default="memory",
|
||||
choices=["memory", "json", "postgres"],
|
||||
help=(
|
||||
"development repository. 'postgres' runs against the authoritative store for testing, "
|
||||
"and requires UMBRATRANS_DATABASE_DSN"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||
|
||||
if os.environ.get("UMBRA_ENV", "").strip().lower() in {"production", "prod"}:
|
||||
|
||||
@@ -880,29 +880,36 @@
|
||||
organizationId = String((state.session && state.session.organization_id) || '');
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
setMessage(message, 'Enter an account name.', 'error');
|
||||
return;
|
||||
}
|
||||
if (!displayName) {
|
||||
setMessage(message, 'Enter a display name.', 'error');
|
||||
return;
|
||||
}
|
||||
if (email && email.indexOf('@') === -1) {
|
||||
setMessage(message, 'Enter a valid email address, or leave it empty.', 'error');
|
||||
if (!email) {
|
||||
setMessage(message, 'Enter an email address. It is required, and it is how the account signs in.', 'error');
|
||||
return;
|
||||
}
|
||||
if (email.indexOf('@') === -1) {
|
||||
setMessage(message, 'Enter a valid email address.', 'error');
|
||||
return;
|
||||
}
|
||||
if (!role) {
|
||||
setMessage(message, 'Choose a role.', 'error');
|
||||
return;
|
||||
}
|
||||
// Every role except platform_admin is organization-scoped, and the store
|
||||
// will not accept a member without a tenant. Caught here so the operator is
|
||||
// told which field to fill in rather than receiving a bare rejection.
|
||||
if (role !== 'platform_admin' && !organizationId) {
|
||||
setMessage(message, 'Choose an organization. Every role except platform administrator belongs to one.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = {
|
||||
username: username,
|
||||
username: username || null,
|
||||
display_name: displayName,
|
||||
enrollment_mode: temporaryRadio && temporaryRadio.checked ? 'temporary_password' : 'oidc',
|
||||
role: role,
|
||||
email: email || null,
|
||||
email: email,
|
||||
organization_id: organizationId || null
|
||||
};
|
||||
|
||||
|
||||
@@ -243,10 +243,26 @@
|
||||
return request('/api/health');
|
||||
}
|
||||
|
||||
function login(username, password, signal) {
|
||||
function login(email, password, signal) {
|
||||
return request('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: { username: username, password: password },
|
||||
body: { email: email, password: password },
|
||||
signal: signal
|
||||
});
|
||||
}
|
||||
|
||||
function verifyEmail(token, signal) {
|
||||
return request('/api/auth/verify-email', {
|
||||
method: 'POST',
|
||||
body: { token: token },
|
||||
signal: signal
|
||||
});
|
||||
}
|
||||
|
||||
function resendVerification(email, signal) {
|
||||
return request('/api/auth/resend-verification', {
|
||||
method: 'POST',
|
||||
body: { email: email },
|
||||
signal: signal
|
||||
});
|
||||
}
|
||||
@@ -694,6 +710,8 @@
|
||||
PASSWORD_MIN_LENGTH: PASSWORD_MIN_LENGTH,
|
||||
health: health,
|
||||
login: login,
|
||||
verifyEmail: verifyEmail,
|
||||
resendVerification: resendVerification,
|
||||
changeFirstPassword: changeFirstPassword,
|
||||
me: me,
|
||||
logout: logout,
|
||||
|
||||
@@ -72,8 +72,20 @@
|
||||
var passwordMessage = query('#password-message');
|
||||
var loginSubmit = query('#login-submit');
|
||||
var passwordSubmit = query('#password-submit');
|
||||
var usernameInput = query('#login-username');
|
||||
var usernameInput = query('#login-email');
|
||||
var passwordInput = query('#login-password');
|
||||
var verifyPanel = query('#verify-panel');
|
||||
var verifyPanelText = query('#verify-panel-text');
|
||||
var verifyPanelMessage = query('#verify-panel-message');
|
||||
var resendButton = query('#resend-verification');
|
||||
var verifyView = query('#verify-view');
|
||||
var verifyViewText = query('#verify-view-text');
|
||||
var verifyForm = query('#verify-form');
|
||||
var verifyTokenInput = query('#verify-token');
|
||||
var verifyMessage = query('#verify-message');
|
||||
var verifySubmit = query('#verify-submit');
|
||||
var verifyBackButton = query('#verify-back-button');
|
||||
var loginView = query('#login-view');
|
||||
var currentInput = query('#current-password');
|
||||
var newInput = query('#new-password');
|
||||
var confirmInput = query('#confirm-password');
|
||||
@@ -82,6 +94,7 @@
|
||||
var serviceNote = query('#service-note');
|
||||
|
||||
var navigationLocked = false;
|
||||
var currentUsername = '';
|
||||
|
||||
function onBeforeUnload(event) {
|
||||
if (!navigationLocked) {
|
||||
@@ -131,6 +144,7 @@
|
||||
api.show(adminLink, false);
|
||||
setNavigationLocked(true);
|
||||
setMessage(passwordMessage, '', null);
|
||||
renderPolicyHint();
|
||||
if (global.history && global.history.replaceState) {
|
||||
global.history.replaceState(null, '', '#change-password');
|
||||
}
|
||||
@@ -162,7 +176,7 @@
|
||||
// Deliberately identical for an unknown account, a wrong password, a
|
||||
// locked account, and a suspended tenant. The response must not be
|
||||
// usable to discover which accounts exist.
|
||||
return 'Sign in failed. Check your account name and password.';
|
||||
return 'Sign in failed. Check your email address and password.';
|
||||
}
|
||||
if (error.code === 'password_policy') {
|
||||
return api.describeRequirements(error);
|
||||
@@ -173,20 +187,22 @@
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var username = usernameInput ? usernameInput.value.trim() : '';
|
||||
var email = usernameInput ? usernameInput.value.trim() : '';
|
||||
var password = passwordInput ? passwordInput.value : '';
|
||||
if (!username || !password) {
|
||||
setMessage(loginMessage, 'Enter your account name and password to continue.', 'error');
|
||||
if (!email || !password) {
|
||||
setMessage(loginMessage, 'Enter your email address and password to continue.', 'error');
|
||||
return;
|
||||
}
|
||||
setMessage(loginMessage, 'Signing in…', null);
|
||||
hideVerifyPanel();
|
||||
busy(true);
|
||||
api.login(username, password).then(
|
||||
api.login(email, password).then(
|
||||
function (data) {
|
||||
busy(false);
|
||||
if (passwordInput) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
currentUsername = (data && data.user && data.user.username) || '';
|
||||
if (data && data.must_change_password) {
|
||||
openPasswordView();
|
||||
return;
|
||||
@@ -199,6 +215,13 @@
|
||||
if (passwordInput) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
// Reached only once the password has already been proven correct, so
|
||||
// naming the reason here does not turn the form into a way to test
|
||||
// whether an address has an account.
|
||||
if (error && error.code === 'email_not_verified') {
|
||||
showVerifyPanel(email);
|
||||
return;
|
||||
}
|
||||
setMessage(loginMessage, describeLoginFailure(error), 'error');
|
||||
if (usernameInput) {
|
||||
usernameInput.focus();
|
||||
@@ -208,6 +231,156 @@
|
||||
});
|
||||
}
|
||||
|
||||
function showVerifyPanel(email) {
|
||||
if (verifyPanelText) {
|
||||
verifyPanelText.textContent =
|
||||
'Your password is correct, but ' + email + ' has not been confirmed yet. ' +
|
||||
'Open the confirmation link in your inbox, or send a new one below.';
|
||||
}
|
||||
if (verifyPanel) {
|
||||
verifyPanel.hidden = false;
|
||||
}
|
||||
if (loginMessage) {
|
||||
setMessage(loginMessage, '', null);
|
||||
}
|
||||
}
|
||||
|
||||
function hideVerifyPanel() {
|
||||
if (verifyPanel) {
|
||||
verifyPanel.hidden = true;
|
||||
}
|
||||
if (verifyPanelMessage) {
|
||||
setMessage(verifyPanelMessage, '', null);
|
||||
}
|
||||
}
|
||||
|
||||
if (resendButton) {
|
||||
resendButton.addEventListener('click', function () {
|
||||
var email = usernameInput ? usernameInput.value.trim() : '';
|
||||
if (!email) {
|
||||
setMessage(verifyPanelMessage, 'Enter your email address above first.', 'error');
|
||||
return;
|
||||
}
|
||||
api.disable(resendButton, true);
|
||||
setMessage(verifyPanelMessage, 'Sending a new link…', null);
|
||||
api.resendVerification(email).then(
|
||||
function () {
|
||||
api.disable(resendButton, false);
|
||||
// The same answer whether or not the address has an account, so this
|
||||
// wording is deliberate and must not be made more specific.
|
||||
setMessage(
|
||||
verifyPanelMessage,
|
||||
'If that address has an account awaiting confirmation, a new link is on its way.',
|
||||
'success'
|
||||
);
|
||||
},
|
||||
function (error) {
|
||||
api.disable(resendButton, false);
|
||||
setMessage(verifyPanelMessage, api.describeError(error), 'error');
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function showVerifyView() {
|
||||
if (loginView) {
|
||||
loginView.hidden = true;
|
||||
}
|
||||
if (verifyView) {
|
||||
verifyView.hidden = false;
|
||||
}
|
||||
if (verifyTokenInput) {
|
||||
verifyTokenInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoginView() {
|
||||
if (verifyView) {
|
||||
verifyView.hidden = true;
|
||||
}
|
||||
if (loginView) {
|
||||
loginView.hidden = false;
|
||||
}
|
||||
if (usernameInput) {
|
||||
usernameInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// A confirmation link is a URL, so arriving from email means the token is in
|
||||
// the address. It is pulled out of the fragment or the query and removed from
|
||||
// the visible address so it is not left sitting in history or a screenshot.
|
||||
function tokenFromLocation() {
|
||||
var hash = global.location.hash || '';
|
||||
var query = global.location.search || '';
|
||||
var match = hash.match(/token=([^&]+)/) || query.match(/token=([^&]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
if (verifyBackButton) {
|
||||
verifyBackButton.addEventListener('click', showLoginView);
|
||||
}
|
||||
|
||||
if (verifyForm) {
|
||||
verifyForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var token = verifyTokenInput ? verifyTokenInput.value.trim() : '';
|
||||
if (!token) {
|
||||
setMessage(verifyMessage, 'Paste the confirmation link or token you were sent.', 'error');
|
||||
return;
|
||||
}
|
||||
setMessage(verifyMessage, 'Confirming your address…', null);
|
||||
api.disable(verifySubmit, true);
|
||||
api.verifyEmail(token).then(
|
||||
function () {
|
||||
api.disable(verifySubmit, false);
|
||||
setMessage(verifyMessage, 'Address confirmed. You can sign in now.', 'success');
|
||||
global.history.replaceState(null, '', global.location.pathname);
|
||||
if (verifyTokenInput) {
|
||||
verifyTokenInput.value = '';
|
||||
}
|
||||
global.setTimeout(showLoginView, 1200);
|
||||
},
|
||||
function (error) {
|
||||
api.disable(verifySubmit, false);
|
||||
setMessage(verifyMessage, api.describeError(error), 'error');
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
(function openVerificationIfRequested() {
|
||||
var token = tokenFromLocation();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
showVerifyView();
|
||||
if (verifyTokenInput) {
|
||||
verifyTokenInput.value = token;
|
||||
}
|
||||
if (verifyMessage) {
|
||||
setMessage(verifyMessage, 'Confirming your address…', null);
|
||||
}
|
||||
if (verifySubmit) {
|
||||
api.disable(verifySubmit, true);
|
||||
}
|
||||
api.verifyEmail(token).then(
|
||||
function () {
|
||||
if (verifySubmit) {
|
||||
api.disable(verifySubmit, false);
|
||||
}
|
||||
setMessage(verifyMessage, 'Address confirmed. You can sign in now.', 'success');
|
||||
global.history.replaceState(null, '', global.location.pathname);
|
||||
global.setTimeout(showLoginView, 1200);
|
||||
},
|
||||
function (error) {
|
||||
if (verifySubmit) {
|
||||
api.disable(verifySubmit, false);
|
||||
}
|
||||
setMessage(verifyMessage, api.describeError(error), 'error');
|
||||
}
|
||||
);
|
||||
}());
|
||||
|
||||
if (passwordForm) {
|
||||
passwordForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
@@ -314,17 +487,24 @@
|
||||
}
|
||||
|
||||
var policyUsername = query('#policy-username');
|
||||
if (policyUsername && usernameInput) {
|
||||
var describePolicy = function () {
|
||||
var name = usernameInput.value.trim();
|
||||
if (!name) {
|
||||
policyUsername.textContent = 'Must not contain your account name.';
|
||||
return;
|
||||
}
|
||||
policyUsername.textContent = 'Must not contain “' + name + '”.';
|
||||
};
|
||||
usernameInput.addEventListener('input', describePolicy);
|
||||
describePolicy();
|
||||
if (policyUsername) {
|
||||
policyUsername.textContent = 'Must not contain your account name.';
|
||||
renderPolicyHint();
|
||||
}
|
||||
|
||||
// The policy forbids a password containing the account's username, which is a
|
||||
// display handle and is often absent. It is deliberately not the sign-in
|
||||
// field: that collects an email address, and telling the user their password
|
||||
// must avoid their own email while the rule checks something else would be
|
||||
// worse than saying nothing.
|
||||
function renderPolicyHint() {
|
||||
if (!policyUsername) {
|
||||
return;
|
||||
}
|
||||
var handle = currentUsername || '';
|
||||
policyUsername.textContent = handle
|
||||
? 'Must not contain “' + handle + '”.'
|
||||
: 'Must not contain your account name.';
|
||||
}
|
||||
|
||||
wirePasswordToggle('toggle-login-password', 'login-password');
|
||||
|
||||
@@ -38,7 +38,12 @@ button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible
|
||||
|
||||
/* Shared brand and status language */
|
||||
.brand-lockup { display: inline-flex; align-items: center; gap: .7rem; color: var(--admin-lilac); font-weight: 800; letter-spacing: -.02em; text-decoration: none; font-size: 1.1rem; }
|
||||
.brand-image { display: block; width: 2.55rem; height: 2.55rem; border-radius: .72rem; object-fit: cover; box-shadow: 0 8px 22px rgba(124, 58, 237, .32); }
|
||||
/* The emblem is a book shape on a transparent field, not a filled rounded tile.
|
||||
The radius therefore does nothing, and the glow is applied with `filter`
|
||||
rather than `box-shadow`: a box-shadow follows the border box instead of the
|
||||
artwork's alpha, so the strong drop the previous filled icon carried would
|
||||
read as a glowing rectangle behind the book. */
|
||||
.brand-image { display: block; width: 2.55rem; height: 2.55rem; object-fit: contain; filter: drop-shadow(0 6px 14px rgba(124, 58, 237, .38)); }
|
||||
.brand-lockup--rail .brand-image { width: 2.35rem; height: 2.35rem; }
|
||||
.eyebrow { margin: 0 0 .45rem; color: var(--admin-lilac); font-size: .72rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
|
||||
.eyebrow--teal { color: var(--workspace-teal); }
|
||||
@@ -86,6 +91,13 @@ button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible
|
||||
.form-message { min-height: 1.2rem; margin: .35rem 0 .2rem; color: var(--admin-muted); font-size: .75rem; }
|
||||
.form-message--error { color: #fca5a5; }
|
||||
.form-message--success { color: #6ee7b7; }
|
||||
/* Shown only once the password has already been proven correct, so it can say
|
||||
why sign-in stopped without revealing whether an address has an account. */
|
||||
.verify-panel { margin: 1.1rem 0 .2rem; padding: 1rem; background: rgba(30, 58, 95, .38); border: 1px solid rgba(147, 197, 253, .28); border-radius: .7rem; }
|
||||
.verify-panel h3 { margin: 0 0 .4rem; color: #dbeafe; font-size: .88rem; }
|
||||
.verify-panel p { margin: 0 0 .8rem; color: var(--admin-muted); font-size: .78rem; line-height: 1.6; }
|
||||
.verify-panel .button { width: 100%; }
|
||||
.verify-panel .form-message { margin-bottom: 0; }
|
||||
.auth-divider { display: flex; align-items: center; gap: .8rem; margin: 1.6rem 0 1rem; color: #64748b; font-size: .7rem; }
|
||||
.auth-divider::before, .auth-divider::after { content: ""; height: 1px; flex: 1; background: var(--admin-border); }
|
||||
.admin-link { display: flex; justify-content: space-between; padding: .8rem .9rem; color: var(--admin-lilac); background: rgba(124, 58, 237, .08); border: 1px solid rgba(167, 139, 250, .3); border-radius: .55rem; font-size: .8rem; font-weight: 750; text-decoration: none; }
|
||||
|
||||
BIN
web/assets/umbratrans-emblem.png
Normal file
BIN
web/assets/umbratrans-emblem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 515 KiB |
@@ -5,14 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="UmbraTrans workspace sign in">
|
||||
<title>Sign in · UmbraTrans</title>
|
||||
<link rel="icon" type="image/jpeg" href="assets/umbratrans-icon.jpg">
|
||||
<link rel="icon" type="image/png" href="assets/umbratrans-emblem.png">
|
||||
<link rel="stylesheet" href="assets/styles.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<main class="auth-shell">
|
||||
<section class="auth-brand" aria-labelledby="brand-title">
|
||||
<a class="brand-lockup" href="index.html" aria-label="UmbraTrans home">
|
||||
<img class="brand-image" src="assets/umbratrans-icon.jpg" alt="" aria-hidden="true">
|
||||
<img class="brand-image" src="assets/umbratrans-emblem.png" alt="" aria-hidden="true">
|
||||
<span>UmbraTrans</span>
|
||||
</a>
|
||||
<div class="brand-copy">
|
||||
@@ -33,11 +33,11 @@
|
||||
<div class="auth-card-heading">
|
||||
<span class="status-pill status-pill--info">Secure workspace</span>
|
||||
<h2 id="signin-title">Sign in to your workspace</h2>
|
||||
<p>Use your UmbraTrans account name and password. Access is determined by your organization membership and role.</p>
|
||||
<p>Use the email address your account was issued with, and your password. Access is determined by your organization membership and role.</p>
|
||||
</div>
|
||||
<form id="login-form" class="stack-form" novalidate>
|
||||
<label for="login-username">Account name</label>
|
||||
<input id="login-username" name="username" type="text" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" placeholder="Your account name" required>
|
||||
<label for="login-email">Email address</label>
|
||||
<input id="login-email" name="email" type="email" inputmode="email" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" placeholder="you@example.com" required>
|
||||
<label for="login-password">Password</label>
|
||||
<div class="password-field">
|
||||
<input id="login-password" name="password" type="password" autocomplete="current-password" placeholder="Enter your password" required>
|
||||
@@ -46,6 +46,27 @@
|
||||
<div id="login-message" class="form-message" role="status" aria-live="polite"></div>
|
||||
<button id="login-submit" class="button button--primary button--wide" type="submit">Sign in <span aria-hidden="true">→</span></button>
|
||||
</form>
|
||||
<div id="verify-panel" class="verify-panel" hidden>
|
||||
<h3>Confirm your email address</h3>
|
||||
<p id="verify-panel-text">Your password is correct, but this address has not been confirmed yet. Check your inbox for the confirmation link, or send a new one.</p>
|
||||
<button id="resend-verification" class="button button--ghost" type="button">Send a new link</button>
|
||||
<div id="verify-panel-message" class="form-message" role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<div id="verify-view" hidden>
|
||||
<div class="auth-card-heading">
|
||||
<span class="status-pill status-pill--info">Almost there</span>
|
||||
<h2>Confirm your email address</h2>
|
||||
<p id="verify-view-text">Paste the confirmation link you were sent.</p>
|
||||
</div>
|
||||
<form id="verify-form" class="stack-form" novalidate>
|
||||
<label for="verify-token">Confirmation link or token</label>
|
||||
<input id="verify-token" name="token" type="text" autocomplete="off" autocapitalize="none" autocorrect="off" spellcheck="false" placeholder="Paste the link, or just the token" required>
|
||||
<div id="verify-message" class="form-message" role="status" aria-live="polite"></div>
|
||||
<button id="verify-submit" class="button button--primary button--wide" type="submit">Confirm my address <span aria-hidden="true">→</span></button>
|
||||
</form>
|
||||
<div id="verify-back" class="auth-divider"><span>Back to sign in</span></div>
|
||||
<button id="verify-back-button" class="admin-link" type="button">Return to sign in</button>
|
||||
</div>
|
||||
<div id="admin-entry" class="auth-divider"><span>Administrator?</span></div>
|
||||
<a id="admin-link" class="admin-link" href="uadmin/"><span>Open control panel</span><span aria-hidden="true">↗</span></a>
|
||||
<p class="auth-footnote">An account issued a temporary password must replace it at first sign-in before any workspace access is granted.</p>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="UmbraTrans account and organization administration">
|
||||
<title>Control panel · UmbraTrans</title>
|
||||
<link rel="icon" type="image/jpeg" href="../assets/umbratrans-icon.jpg">
|
||||
<link rel="icon" type="image/png" href="../assets/umbratrans-emblem.png">
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-shell">
|
||||
<header class="admin-topbar">
|
||||
<a class="brand-lockup" href="../index.html"><img class="brand-image" src="../assets/umbratrans-icon.jpg" alt="" aria-hidden="true"><span>UmbraTrans</span></a>
|
||||
<a class="brand-lockup" href="../index.html"><img class="brand-image" src="../assets/umbratrans-emblem.png" alt="" aria-hidden="true"><span>UmbraTrans</span></a>
|
||||
<div class="admin-context"><span class="status-pill status-pill--info">Control panel</span><span class="admin-context-name">Identity administration</span></div>
|
||||
<div class="admin-top-actions">
|
||||
<span class="admin-user">
|
||||
@@ -274,9 +274,9 @@
|
||||
<form id="provision-form" class="stack-form" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label for="provision-username">Account name</label>
|
||||
<input id="provision-username" name="username" type="text" autocomplete="off" autocapitalize="none" spellcheck="false" maxlength="64" required>
|
||||
<p class="field-hint" id="provision-username-hint">The name used at sign-in, not an email address.</p>
|
||||
<label for="provision-username">Account name <span class="field-optional">optional</span></label>
|
||||
<input id="provision-username" name="username" type="text" autocomplete="off" autocapitalize="none" spellcheck="false" maxlength="64">
|
||||
<p class="field-hint" id="provision-username-hint">A display handle. Sign-in uses the email address, not this.</p>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="provision-display-name">Display name</label>
|
||||
@@ -284,8 +284,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="provision-email">Email address <span class="field-optional">optional</span></label>
|
||||
<input id="provision-email" name="email" type="email" autocomplete="off" maxlength="254">
|
||||
<label for="provision-email">Email address</label>
|
||||
<input id="provision-email" name="email" type="email" autocomplete="off" maxlength="254" required>
|
||||
<p class="field-hint">Required, and the identifier the account signs in with. A confirmation link is sent to it.</p>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="UmbraTrans project workspace">
|
||||
<title>Workspace · UmbraTrans</title>
|
||||
<link rel="icon" type="image/jpeg" href="assets/umbratrans-icon.jpg">
|
||||
<link rel="icon" type="image/png" href="assets/umbratrans-emblem.png">
|
||||
<link rel="stylesheet" href="assets/styles.css">
|
||||
</head>
|
||||
<body class="workspace-page">
|
||||
<div class="workspace-shell">
|
||||
<aside class="workspace-rail" aria-label="Workspace navigation">
|
||||
<a class="brand-lockup brand-lockup--rail" href="index.html">
|
||||
<img class="brand-image" src="assets/umbratrans-icon.jpg" alt="" aria-hidden="true">
|
||||
<img class="brand-image" src="assets/umbratrans-emblem.png" alt="" aria-hidden="true">
|
||||
<span>UmbraTrans</span>
|
||||
</a>
|
||||
<div class="rail-section-label">Workspace</div>
|
||||
|
||||
Reference in New Issue
Block a user