The page where a user changes their own password and the one where an
administrator sets a user's password shared an old card: a horizontal
form, a blue Cancel and a green Change. The administrator's had Django's
"Password-based authentication: Enabled/Disabled" with a paragraph about
other backends, and its password fields stayed while Disabled was chosen.
Both pages now have the user and group forms' layout, each its own
template. The administrator chooses how the user signs in, With a
password or Without a password (such as LDAP), and the password fields
and "must change it" show only with a password; Cancel goes back to the
user. A user's own page says that their other sessions end, or, when
they must change it, that they choose one before going on, without a
Cancel. A refused password is said at its field only, without the
general "Wrong Data Provided". The section styles of these forms move
into webvirtcloud.css.
A step towards a Content-Security-Policy without inline scripts. The
scripts every page or many pages share now load from static files: the
error focus of base.html, the please-wait dialog, the sign-in forms'
password toggle and busy button, the jobs pages' reload while a job
runs, and the start of the job panel and of the VM list's state poll,
whose values come as json_script data. Cancel links go back through a
data-history-back attribute instead of a javascript: URL. The IE8
html5shiv/respond.js block, X-UA-Compatible and shrink-to-fit are gone:
no supported browser reads them.
A superuser's page of a user was a heading, two unlabelled icon buttons
and three tabs: the VMs a table of check columns, the SSH keys a table
that pushed the page sideways on a phone.
Now a card at the top shows the user's name, role, Blocked when they
are, username, email, groups, last login and joining date, with Edit
user and Email OTP QR code as labelled buttons and links to the sections
below with their counts. The VMs card shows each VM with its host and
what the user may do beyond power and console as badges in the VM Users
tab's words (Change settings, Delete the VM, Change console settings),
with Change access and Remove from user in a menu, and Add VM in its
header. The SSH keys and the API tokens are cards; the key rows are now
one include with the user's own Profile, and both lists say when they
are empty. The full key shows over its fingerprint.
The tab script had no page left and goes.
The Profile page was three tabs, each a box inside a box with horizontal
labels: the SSH keys a centred bare row with a grey trash icon, a tall
empty textarea under them, and the user known only by the heading.
Now the user's name, username, role, email and last login head a card
that holds the profile form and the password link; beside it the SSH
keys and the API tokens are cards with their count, a line on what
they are for, one row each with Delete in a menu, and the form to add
one behind a button. Adding or deleting a key returns to #sshkeys.
The token rows are a list-group include that the administrator's view
of a user shares.
static/css/signin.css (308 lines) gave the sign-in and two-factor
sign-in pages their own components: a card, input groups, a submit
button, a footer and a status dot, with gradients, a blurred card,
entry and pulse animations, and rules that undid base.html's layout
(its container, its padding) from a "signin-page" class the pages'
scripts put on the body.
The pages are Bootstrap now: a card in the grid's centre column, the
logo a rounded-3 bg-primary block, the fields Bootstrap input-groups
with the password toggle an outline button, the submit a w-100
btn-primary, the footer a border-top line with a rounded-circle dot.
They follow the selected Bootswatch theme like every other page.
signin.css, its body class and its decoration go; so does the footer's
"v2.0.0", a version the panel never had.
The sign-out page (logout.html) was never shown: LOGOUT_REDIRECT_URL
sends a signed-out user to the sign-in page. It goes, with the view's
template name.
webvirtcloud.css still carried the upstream panel's rules for what
Bootstrap 5.3 has as utilities and components:
- the page's bottom space (body padding) is pb-5 on base.html's body,
and the console page no longer has to undo it;
- the VM page's action tabs (.action-button, .action-block with a
fixed 65px width) are text-center with a d-block icon; its tab panes
(#power ... #undefine, one rule each) mt-3;
- the bordered sub-tab panes (.tab-pane-bordered, a fixed #eee border
that dark themes could not change) border border-top-0 p-3;
- the search box and header buttons (.search, .btn-header) m-1 and
mt-1; the SSH key select (.keyselect) a form-select;
- the boot order list (.multipleselect, fixed size, #c0c0c0 border) a
list-group of form-check-input items; its unused .multipleselect-on
goes;
- table cells centred by a global rule are align-middle on each table.
Two rules against Bootstrap's behaviour go: menus opened on hover on
wide screens (Bootstrap opens them on a click, and keyboard and touch
users had no hover), and "a { outline: 0 }", which hid the keyboard
focus ring.
The paragraph and page-header margins and the 900px container stay
for a separate change: they set every page's layout.
- An SSH public key whose comment holds spaces (ssh-keygen -C "John Doe
laptop") is accepted; only the type and the key are checked.
- The created API token is shown by a GET after a redirect, so reloading
the page no longer creates a second token the user never sees.
- The OTP login page shows field errors instead of an empty error box.
- A user without a local password (an LDAP user) is not offered a password
change that could never succeed.
On a superuser's page of a user the VM permissions read True/False; they
are now check marks as on the Users list. A user without VMs gets "No VMs."
instead of an empty table. A superuser can delete one of the user's SSH
keys, as with API tokens: the same view takes the user, the row names
whose key it was.
Closes#218
- An administrator could block, delete or demote (unset Active or
Superuser on) their own account, and with it the last one, leaving the
panel without anyone to undo it. These are now refused.
- The user form showed "No password set." for users who have one: its
password field never read the user's password. Its "Set password"
link went to ../password/, a page this panel does not have. The form
now says whether a password is set and links the page that sets it.
- "Staff status" said "can log into this admin site"; here it lets a
user change and delete the template VMs given to them, and is now
called Template editor.
- A quota of 0 counted as no limit, like -1, so a user's quota could not
be taken away. Now -1 has no limit and 0 allows none; the help says
so, and the labels give the units: memory in MiB, disk in GiB.
- A user's page for a user that does not exist answered with a server
error instead of not found.
- The user list said "None" for a user who never logged in; it says
"never", and dates fit their column.
- The password form's labels no longer break mid-word.
The user page a superuser opens from the user list has an API Tokens
tab: the user's tokens with their name, first characters, creation,
expiry and last use, and a delete button for one that may have leaked.
The token itself is never shown; only its digest is kept.
The delete reuses the owner's delete view with the user's id; only a
superuser may name another user. The confirmation and the log row say
whose token it was ("API token backup of alice"); the owner's own
confirmation now reads "Delete "API token backup"?" too. The profile
and the user page share one token table, and one script that opens a
tab named in the address (#apitokens).
The API Tokens tab gets a "Last used" column, so an unused token can be
told from one a script still needs. The token's last use is written when
it authenticates, at most once a minute (accounts.auth.TokenAuthentication,
knox's class with that one addition; the API schema describes it as
knox's).
A user has at most ten tokens; a new one is refused until one is
deleted. Expired tokens do not count, and are deleted when a new token is
made.
The REST API took only the login session, so a script could not use it
without a browser. A user now makes API tokens on the profile page (API
Tokens tab): a name, and an expiry of 30, 90 or 365 days or none. The
token is shown once, on a page that is not cached, and then kept only as
a SHA-512 digest; the list shows its name, first characters, creation
and expiry, and a token is deleted there. Tools send it as
"Authorization: Token wvc_...", and act as its user, with that user's
permissions. Making and deleting a token is logged, without the token.
A password alone (HTTP Basic) still does not reach the API, as it would
skip the OTP second factor: a token is made in a session that passed the
login and its OTP. The token class comes first, so a refused request gets
401 with its challenge instead of 403.
django-rest-knox 5.1.0 provides the tokens (accounts.ApiToken adds the
name); settings.py takes "knox", KNOX_TOKEN_MODEL and REST_KNOX from the
template.
Column offsets without a breakpoint applied below it, where the column
is already full width, so the change password, compute, user, VM
create and VM settings forms ran past the right edge on a phone. Each
offset now starts at its column's breakpoint. The interface and network
detail lists were rows directly inside rows and sit in a column now;
the compute form loses a Bootstrap 3 thumbnail padding that broke its
gutters, and the add-rule dialog's hint is no longer cut off. The page
test checks both grid rules on every page.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Twenty tables had no .table-responsive wrapper, so on a phone the user
list, compute list, VM lists, network and network filter pages scrolled
sideways as a whole. Each table now scrolls inside its box, as the
other ten already did. The page test walks the network page too, and
checks that every table on every page has the wrapper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
django-qr-code was used for one thing: the QR code in the OTP e-mail,
as an inline SVG and a PNG. It brought pydantic, pydantic-core,
annotated-types and typing-inspection (about 10 MB) and, from 5.0 on,
Pillow. segno, which it used underneath, now draws both images with the
same options (error correction M, 4-module border, 1.8 mm SVG modules,
12 px PNG modules); the SVG, the PNG and the alt text are byte for byte
what django-qr-code 4.2 produced. account.html loaded the tag library
without using it.
With OTP_ENABLED the login used django-otp's OTPAuthenticationForm. Under the
pinned django-otp 1.7.3 that form could not log anyone in through the shipped
page: the page posts only a 6-digit otp_token, but the form now needs the
device named explicitly, so an enrolled user got "Please select a device.",
and a user who never enrolled got "Please enter your OTP token." with no
device to produce one. Verified in an isolated test database.
SingleDeviceOTPForm makes OTP per user: a user with a confirmed TOTP device
must enter a valid code; a user without one logs in with the password alone.
The form chooses the device itself (the user's oldest confirmed TOTP device,
locked with select_for_update inside clean_otp's transaction so replay and
throttling counters cannot race) and ignores any posted otp_device, which
django-otp does not check for confirmed status or type. The login page drops
the HTML "required" on the token field (the field is optional for an
unenrolled user) and says so.
The "Lost OTP?" page (email_otp) created a confirmed device when none existed.
With per-user OTP that let anyone who knows an address turn a password-only
account into an OTP-required one and, if the mail never arrived, lock it out.
It now resends the QR only to a user who already enrolled and never creates a
device (get_existing_totp_device); admin enrollment (admin_email_otp) still
creates one. Email QR distribution and confirmed-on-create enrollment are
unchanged by user decision.
Tests: per-user login (no device -> password only; device -> valid token
passes, wrong/empty token fails), a posted or another user's device is not
used, a token cannot be replayed, and the real login page over HTTP with
OTP_ENABLED; email_otp sends nothing and creates no device for an unenrolled
user. The new tests fail on the previous code.
- user_block, user_unblock and admin_email_otp changed state on GET, so an
<img> tag could lock a user out or trigger OTP emails through an
admin's session. They now require POST; the buttons are CSRF forms.
- The console token cookie is set with httponly, SameSite=Lax and, on
HTTPS, secure. Only novncd reads it; the page gets the token directly.
- Replace hardcoded colors with Bootstrap & Bootswatch CSS custom properties
- Adapt background, card, borders, and inputs dynamically to active light and dark themes
- Inherit primary color, border-radius, and font-family from active theme
- Use native Bootswatch alert and button styles for consistent visual hierarchy
- Overhaul signin.css with responsive dark glassmorphic card and ambient lighting
- Add server virtualization branding badge and typography hierarchy
- Enhance input fields with leading Bootstrap icons and interactive password visibility toggle
- Modernize 2FA/OTP login and signed-out pages to match design language
- Add real-time loading feedback on form submission