Compare commits

...

2 Commits

Author SHA1 Message Date
Chocobo1
0c17d91d63 Use Permanent Cookie
Previously, WebUI was using a HTTP Session Cookie. This type of cookie is tend to be dropped by
the browser on mobile platforms and gives a bad experience on the WebUI. Now the cookie is a
permanent one and is guaranteed to be persisted between browser restarts.

Closes #20993.
PR #23392.
2025-10-25 03:37:46 +08:00
Marcus Järrud
60feb3cce2 WebUI: Redesign login form
Make the Web UI login more mobile friendly.
The main action is moving to a column layout for the logo and form. This way it's easier for mobile to view the page but it doesn't affect the desktop experience much.

Secondary it set the input font size to be default text size, this in turn makes it so the mobile ui doesn't zoom in when you focus the inputs. [You can read more about it here](https://wsform.com/knowledgebase/why-forms-zoom-on-some-mobile-devices-and-browsers-and-how-to-control-it/).

PR #23360.
2025-10-25 00:23:18 +08:00
5 changed files with 74 additions and 29 deletions

View File

@@ -51,6 +51,7 @@ namespace Http
inline const QString HEADER_CONTENT_LENGTH = u"content-length"_s;
inline const QString HEADER_CONTENT_SECURITY_POLICY = u"content-security-policy"_s;
inline const QString HEADER_CONTENT_TYPE = u"content-type"_s;
inline const QString HEADER_COOKIE = u"cookie"_s;
inline const QString HEADER_CROSS_ORIGIN_OPENER_POLICY = u"cross-origin-opener-policy"_s;
inline const QString HEADER_DATE = u"date"_s;
inline const QString HEADER_HOST = u"host"_s;

View File

@@ -30,7 +30,6 @@
#include "webapplication.h"
#include <algorithm>
#include <chrono>
#include <QDateTime>
#include <QDebug>
@@ -88,7 +87,7 @@ namespace
QStringMap ret;
const QList<QStringView> cookies = cookieStr.split(u';', Qt::SkipEmptyParts);
for (const auto &cookie : cookies)
for (const QStringView cookie : cookies)
{
const qsizetype idx = cookie.indexOf(u'=');
if (idx < 0)
@@ -462,9 +461,13 @@ void WebApplication::configure()
m_isLocalAuthEnabled = pref->isWebUILocalAuthEnabled();
m_isAuthSubnetWhitelistEnabled = pref->isWebUIAuthSubnetWhitelistEnabled();
m_authSubnetWhitelist = pref->getWebUIAuthSubnetWhitelist();
m_sessionTimeout = pref->getWebUISessionTimeout();
m_sessionTimeout = std::chrono::seconds(pref->getWebUISessionTimeout());
m_sessionCookieName = SESSION_COOKIE_NAME_PREFIX + QString::number(pref->getWebUIPort());
// all sessions need to update the cookie expiration date
for (WebSession *session : asConst(m_sessions))
session->setCookieRefreshTime(0s);
m_domainList = pref->getServerDomains().split(u';', Qt::SkipEmptyParts);
for (QString &entry : m_domainList)
entry = entry.trimmed();
@@ -682,7 +685,7 @@ void WebApplication::sessionInitialize()
{
Q_ASSERT(!m_currentSession);
const QString sessionId {parseCookie(m_request.headers.value(u"cookie"_s)).value(m_sessionCookieName)};
const QString sessionId {parseCookie(m_request.headers.value(Http::HEADER_COOKIE)).value(m_sessionCookieName)};
// TODO: Additional session check
@@ -699,19 +702,36 @@ void WebApplication::sessionInitialize()
}
else
{
if (m_currentSession->shouldRefreshCookie())
setSessionCookie();
m_currentSession->updateTimestamp();
}
}
else
{
qDebug() << Q_FUNC_INFO << "session does not exist!";
}
}
if (!m_currentSession && !isAuthNeeded())
sessionStart();
}
void WebApplication::setSessionCookie()
{
// 'Permanent Cookie' still require an expiration date so set it to a date in the distant future
const std::chrono::seconds expireDuration = (m_sessionTimeout > 0s) ? m_sessionTimeout : std::chrono::years(1);
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
cookie.setExpirationDate(QDateTime::currentDateTime().addDuration(expireDuration));
cookie.setHttpOnly(true);
cookie.setSecure(m_isSecureCookieEnabled && isOriginTrustworthy()); // [rfc6265] 4.1.2.5. The Secure Attribute
cookie.setPath(u"/"_s);
if (m_isCSRFProtectionEnabled)
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
else if (cookie.isSecure())
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
m_currentSession->setCookieRefreshTime(expireDuration);
}
void WebApplication::apiKeySessionInitialize()
{
Q_ASSERT(!m_currentSession);
@@ -807,17 +827,7 @@ void WebApplication::sessionStartImpl(const QString &sessionId, const bool useCo
m_currentSession->registerAPIController(u"sync"_s, syncController);
if (useCookie)
{
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
cookie.setHttpOnly(true);
cookie.setSecure(m_isSecureCookieEnabled && isOriginTrustworthy()); // [rfc6265] 4.1.2.5. The Secure Attribute
cookie.setPath(u"/"_s);
if (m_isCSRFProtectionEnabled)
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
else if (cookie.isSecure())
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
}
setSessionCookie();
}
void WebApplication::sessionEnd()
@@ -986,16 +996,29 @@ QString WebSession::id() const
return m_sid;
}
bool WebSession::hasExpired(const qint64 seconds) const
bool WebSession::hasExpired(const std::chrono::milliseconds duration) const
{
if (seconds <= 0)
// don't expire for special values
if (duration <= 0ms)
return false;
return m_timer.hasExpired(seconds * 1000);
return m_timestamp.durationElapsed() > duration;
}
void WebSession::updateTimestamp()
{
m_timer.start();
m_timestamp.start();
}
bool WebSession::shouldRefreshCookie() const
{
return m_cookieRefreshTimer.hasExpired();
}
void WebSession::setCookieRefreshTime(const std::chrono::seconds timeout)
{
// Safari browser does not persist cookies for more than 7 days, so we refresh cookies older than 1 day
const std::chrono::seconds time = std::min((timeout / 2), std::chrono::duration_cast<std::chrono::seconds>(std::chrono::days(1)));
m_cookieRefreshTimer.setRemainingTime(time);
}
void WebSession::registerAPIController(const QString &scope, APIController *controller)

View File

@@ -29,10 +29,12 @@
#pragma once
#include <chrono>
#include <type_traits>
#include <utility>
#include <QDateTime>
#include <QDeadlineTimer>
#include <QElapsedTimer>
#include <QHash>
#include <QHostAddress>
@@ -53,6 +55,8 @@
#include "base/utils/version.h"
#include "api/isessionmanager.h"
using namespace std::chrono_literals;
inline const Utils::Version<3, 2> API_VERSION {2, 14, 1};
class APIController;
@@ -72,15 +76,18 @@ public:
QString id() const override;
bool hasExpired(qint64 seconds) const;
bool hasExpired(std::chrono::milliseconds duration) const;
void updateTimestamp();
bool shouldRefreshCookie() const;
void setCookieRefreshTime(std::chrono::seconds timeout);
void registerAPIController(const QString &scope, APIController *controller);
APIController *getAPIController(const QString &scope) const;
private:
const QString m_sid;
QElapsedTimer m_timer; // timestamp
QElapsedTimer m_timestamp;
QDeadlineTimer m_cookieRefreshTimer;
QMap<QString, APIController *> m_apiControllers;
};
@@ -123,6 +130,7 @@ private:
// Session management
QString generateSid() const;
void sessionInitialize();
void setSessionCookie();
void apiKeySessionInitialize();
bool isAuthNeeded();
bool isPublicAPI(const QString &scope, const QString &action) const;
@@ -247,7 +255,7 @@ private:
bool m_isLocalAuthEnabled = false;
bool m_isAuthSubnetWhitelistEnabled = false;
QList<Utils::Net::Subnet> m_authSubnetWhitelist;
int m_sessionTimeout = 0;
std::chrono::seconds m_sessionTimeout = 0s;
QString m_sessionCookieName;
QString m_apiKey;

View File

@@ -16,12 +16,23 @@ body {
}
#main {
align-items: center;
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: auto;
padding-top: 5em;
text-align: center;
}
.login-input {
box-sizing: border-box;
font-size: 1rem;
margin: 0.5rem 0;
padding: 0.5rem;
width: 100%;
}
#formplace {
padding: 10px;
text-align: left;
@@ -33,7 +44,9 @@ body {
}
#loginButton {
float: right;
font-size: 1rem;
padding: 0.5rem;
width: 100%;
}
#logo img {

View File

@@ -32,11 +32,11 @@
<form id="loginform">
<div class="row">
<label for="username">QBT_TR(Username)QBT_TR[CONTEXT=Login]</label><br>
<input type="text" id="username" name="username" autocomplete="username" autocapitalize="none" autofocus required>
<input type="text" class="login-input" id="username" name="username" autocomplete="username" autocapitalize="none" autofocus required>
</div>
<div class="row">
<label for="password">QBT_TR(Password)QBT_TR[CONTEXT=Login]</label><br>
<input type="password" id="password" name="password" autocomplete="current-password" required>
<input type="password" class="login-input" id="password" name="password" autocomplete="current-password" required>
</div>
<div class="row">
<input type="submit" id="loginButton" value="QBT_TR(Login)QBT_TR[CONTEXT=Login]">