Add support for SSO authentication
This commit is contained in:
parent
d7907db547
commit
157ea2ffb2
4
TODO.md
4
TODO.md
|
@ -1,7 +1,5 @@
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- fallback page
|
|
||||||
- SSO page
|
|
||||||
- sever list
|
- sever list
|
||||||
- cursor shape in HBox/HTabbedBox pages over fields
|
- cursor shape in HBox/HTabbedBox pages over fields
|
||||||
- login with account already added → infinite spinner in room list
|
- login with account already added → infinite spinner in room list
|
||||||
|
@ -208,10 +206,8 @@
|
||||||
- `m.id.thirdparty`
|
- `m.id.thirdparty`
|
||||||
- `m.id.phone`
|
- `m.id.phone`
|
||||||
- `m.login.recaptcha` (need browser, just use fallback?)
|
- `m.login.recaptcha` (need browser, just use fallback?)
|
||||||
- `m.login.oauth2`
|
|
||||||
- `m.login.email.identity`
|
- `m.login.email.identity`
|
||||||
- `m.login.msisdn` (phone)
|
- `m.login.msisdn` (phone)
|
||||||
- `m.login.sso` + `m.login.token`
|
|
||||||
- `m.login.dummy`
|
- `m.login.dummy`
|
||||||
- Web page fallback
|
- Web page fallback
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ from .models.items import Account, Event
|
||||||
from .models.model import Model
|
from .models.model import Model
|
||||||
from .models.model_store import ModelStore
|
from .models.model_store import ModelStore
|
||||||
from .presence import Presence
|
from .presence import Presence
|
||||||
|
from .sso_server import SSOServer
|
||||||
from .user_files import Accounts, History, Theme, UISettings, UIState
|
from .user_files import Accounts, History, Theme, UISettings, UIState
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
|
@ -103,6 +104,9 @@ class Backend:
|
||||||
|
|
||||||
self.clients: Dict[str, MatrixClient] = {}
|
self.clients: Dict[str, MatrixClient] = {}
|
||||||
|
|
||||||
|
self._sso_server: Optional[SSOServer] = None
|
||||||
|
self._sso_server_task: Optional[asyncio.Future] = None
|
||||||
|
|
||||||
self.profile_cache: Dict[str, nio.ProfileGetResponse] = {}
|
self.profile_cache: Dict[str, nio.ProfileGetResponse] = {}
|
||||||
self.get_profile_locks: DefaultDict[str, asyncio.Lock] = \
|
self.get_profile_locks: DefaultDict[str, asyncio.Lock] = \
|
||||||
DefaultDict(asyncio.Lock) # {user_id: lock}
|
DefaultDict(asyncio.Lock) # {user_id: lock}
|
||||||
|
@ -153,21 +157,57 @@ class Backend:
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
async def login_client(self,
|
async def password_auth(
|
||||||
user: str,
|
self, user: str, password: str, homeserver: str,
|
||||||
password: str,
|
|
||||||
device_id: Optional[str] = None,
|
|
||||||
homeserver: str = "https://matrix.org",
|
|
||||||
order: Optional[int] = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create and register a `MatrixClient`, login and return a user ID."""
|
"""Create & register a `MatrixClient`, login using the password
|
||||||
|
and return the user ID we get.
|
||||||
|
"""
|
||||||
|
|
||||||
client = MatrixClient(
|
client = MatrixClient(self, user=user, homeserver=homeserver)
|
||||||
self, user=user, homeserver=homeserver, device_id=device_id,
|
return await self._do_login(client, password=password)
|
||||||
)
|
|
||||||
|
|
||||||
|
async def start_sso_auth(self, homeserver: str) -> str:
|
||||||
|
"""Start SSO server and return URL to open in the user's browser.
|
||||||
|
|
||||||
|
See the `sso_server.SSOServer` class documentation.
|
||||||
|
Once the returned URL has been opened in the user's browser
|
||||||
|
(done from QML), `MatrixClient.continue_sso_auth()` should be called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
server = SSOServer(homeserver)
|
||||||
|
self._sso_server = server
|
||||||
|
self._sso_server_task = asyncio.ensure_future(server.wait_for_token())
|
||||||
|
return server.url_to_open
|
||||||
|
|
||||||
|
|
||||||
|
async def continue_sso_auth(self) -> str:
|
||||||
|
"""Wait for the started SSO server to get a token, then login.
|
||||||
|
|
||||||
|
`MatrixClient.start_sso_auth()` must be called first.
|
||||||
|
Creates and register a `MatrixClient` for logging in.
|
||||||
|
Returns the user ID we get from logging in.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._sso_server or not self._sso_server_task:
|
||||||
|
raise RuntimeError("Must call Backend.start_sso_auth() first")
|
||||||
|
|
||||||
|
await self._sso_server_task
|
||||||
|
homeserver = self._sso_server.for_homeserver
|
||||||
|
token = self._sso_server_task.result()
|
||||||
|
self._sso_server_task = None
|
||||||
|
self._sso_server = None
|
||||||
|
|
||||||
|
client = MatrixClient(self, homeserver=homeserver)
|
||||||
|
return await self._do_login(client, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
async def _do_login(self, client: MatrixClient, **login_kwargs) -> str:
|
||||||
|
"""Create and register the `MatrixClient`, login and return user ID."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await client.login(password, order=order)
|
await client.login(**login_kwargs)
|
||||||
except MatrixError:
|
except MatrixError:
|
||||||
await client.close()
|
await client.close()
|
||||||
raise
|
raise
|
||||||
|
@ -178,7 +218,6 @@ class Backend:
|
||||||
return client.user_id
|
return client.user_id
|
||||||
|
|
||||||
self.clients[client.user_id] = client
|
self.clients[client.user_id] = client
|
||||||
|
|
||||||
return client.user_id
|
return client.user_id
|
||||||
|
|
||||||
|
|
||||||
|
@ -187,7 +226,7 @@ class Backend:
|
||||||
user_id: str,
|
user_id: str,
|
||||||
token: str,
|
token: str,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
homeserver: str = "https://matrix.org",
|
homeserver: str,
|
||||||
state: str = "online",
|
state: str = "online",
|
||||||
status_msg: str = "",
|
status_msg: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -191,6 +191,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
self.upload_tasks: Dict[UUID, asyncio.Task] = {}
|
self.upload_tasks: Dict[UUID, asyncio.Task] = {}
|
||||||
self.send_message_tasks: Dict[UUID, asyncio.Task] = {}
|
self.send_message_tasks: Dict[UUID, asyncio.Task] = {}
|
||||||
|
|
||||||
|
self._presence: str = ""
|
||||||
self.first_sync_done: asyncio.Event = asyncio.Event()
|
self.first_sync_done: asyncio.Event = asyncio.Event()
|
||||||
self.first_sync_date: Optional[datetime] = None
|
self.first_sync_date: Optional[datetime] = None
|
||||||
self.last_sync_error: Optional[Exception] = None
|
self.last_sync_error: Optional[Exception] = None
|
||||||
|
@ -212,6 +213,33 @@ class MatrixClient(nio.AsyncClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def healthy(self) -> bool:
|
||||||
|
"""Return whether we're syncing and last sync was successful."""
|
||||||
|
|
||||||
|
task = self.sync_task
|
||||||
|
|
||||||
|
if not task or not self.first_sync_date or self.last_sync_error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return not task.done()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_device_name(self) -> str:
|
||||||
|
"""Device name to set at login if the user hasn't set a custom one."""
|
||||||
|
|
||||||
|
os_name = platform.system()
|
||||||
|
|
||||||
|
if not os_name: # unknown OS
|
||||||
|
return __display_name__
|
||||||
|
|
||||||
|
# On Linux, the kernel version is returned, so for a one-time-set
|
||||||
|
# device name it would quickly be outdated.
|
||||||
|
os_ver = platform.release() if os_name == "Windows" else ""
|
||||||
|
return f"{__display_name__} on {os_name} {os_ver}".rstrip()
|
||||||
|
|
||||||
|
|
||||||
async def _send(self, *args, **kwargs) -> nio.Response:
|
async def _send(self, *args, **kwargs) -> nio.Response:
|
||||||
"""Raise a `MatrixError` subclass for any `nio.ErrorResponse`.
|
"""Raise a `MatrixError` subclass for any `nio.ErrorResponse`.
|
||||||
|
|
||||||
|
@ -228,54 +256,38 @@ class MatrixClient(nio.AsyncClient):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def default_device_name() -> str:
|
|
||||||
"""Device name to set at login if the user hasn't set a custom one."""
|
|
||||||
|
|
||||||
os_name = platform.system()
|
|
||||||
|
|
||||||
if not os_name: # unknown OS
|
|
||||||
return __display_name__
|
|
||||||
|
|
||||||
# On Linux, the kernel version is returned, so for a one-time-set
|
|
||||||
# device name it would quickly be outdated.
|
|
||||||
os_ver = platform.release() if os_name == "Windows" else ""
|
|
||||||
return f"{__display_name__} on {os_name} {os_ver}".rstrip()
|
|
||||||
|
|
||||||
|
|
||||||
async def login(
|
async def login(
|
||||||
self,
|
self, password: Optional[str] = None, token: Optional[str] = None,
|
||||||
password: str,
|
|
||||||
device_name: str = "",
|
|
||||||
order: Optional[int] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Login to the server using the account's password."""
|
"""Login to server using `m.login.password` or `m.login.token` flows.
|
||||||
|
|
||||||
await super().login(
|
Login can be done with the account's password (if the server supports
|
||||||
password, device_name or self.default_device_name(),
|
this flow) OR a token obtainable through various means.
|
||||||
)
|
|
||||||
|
|
||||||
saved = await self.backend.saved_accounts.read()
|
One of the way to obtain a token is to follow the `m.login.sso` flow
|
||||||
|
first, see `Backend.start_sso_auth()` & `Backend.continue_sso_auth()`.
|
||||||
|
"""
|
||||||
|
|
||||||
if order is None and not saved.values():
|
await super().login(password, self.default_device_name, token)
|
||||||
order = 0
|
|
||||||
elif order is None:
|
order = 0
|
||||||
|
saved_accounts = await self.backend.saved_accounts.read()
|
||||||
|
|
||||||
|
if saved_accounts:
|
||||||
order = max(
|
order = max(
|
||||||
account.get("order", i)
|
account.get("order", i)
|
||||||
for i, account in enumerate(saved.values())
|
for i, account in enumerate(saved_accounts.values())
|
||||||
) + 1
|
) + 1
|
||||||
|
|
||||||
# Get or create account model
|
# We need to create account model item here, because _start() needs it
|
||||||
# We need to create account model in here, because _start() needs it
|
item = self.models["accounts"].setdefault(
|
||||||
account = self.models["accounts"].setdefault(
|
|
||||||
self.user_id, Account(self.user_id, order),
|
self.user_id, Account(self.user_id, order),
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: set presence on login
|
# TODO: be abke to set presence before logging in
|
||||||
self._presence: str = "online"
|
item.set_fields(presence=Presence.State.online, connecting=True)
|
||||||
account.presence = Presence.State.online
|
self._presence = "online"
|
||||||
account.connecting = True
|
self.start_task = asyncio.ensure_future(self._start())
|
||||||
self.start_task = asyncio.ensure_future(self._start())
|
|
||||||
|
|
||||||
|
|
||||||
async def resume(
|
async def resume(
|
||||||
|
@ -299,7 +311,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
if state != "offline":
|
if state != "offline":
|
||||||
account.connecting = True
|
account.connecting = True
|
||||||
self.start_task = asyncio.ensure_future(self._start())
|
self.start_task = asyncio.ensure_future(self._start())
|
||||||
|
|
||||||
|
|
||||||
async def logout(self) -> None:
|
async def logout(self) -> None:
|
||||||
|
@ -326,17 +338,6 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
@property
|
|
||||||
def healthy(self) -> bool:
|
|
||||||
"""Return whether we're syncing and last sync was successful."""
|
|
||||||
|
|
||||||
task = self.sync_task
|
|
||||||
|
|
||||||
if not task or not self.first_sync_date or self.last_sync_error:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return not task.done()
|
|
||||||
|
|
||||||
|
|
||||||
async def _start(self) -> None:
|
async def _start(self) -> None:
|
||||||
"""Fetch our user profile, server config and enter the sync loop."""
|
"""Fetch our user profile, server config and enter the sync loop."""
|
||||||
|
|
99
src/backend/sso_server.py
Normal file
99
src/backend/sso_server.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib.parse import parse_qs, quote, urlparse
|
||||||
|
from . import __display_name__
|
||||||
|
|
||||||
|
_SUCCESS_HTML_PAGE = """<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>""" + __display_name__ + """</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { background: hsl(0, 0%, 90%); }
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
0% { transform: scale(0); }
|
||||||
|
45% { transform: scale(0); }
|
||||||
|
80% { transform: scale(1.6); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin: -45px 0 0 -45px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 60px;
|
||||||
|
line-height: 90px;
|
||||||
|
text-align: center;
|
||||||
|
background: hsl(203, 51%, 15%);
|
||||||
|
color: hsl(162, 56%, 42%, 1);
|
||||||
|
animation: appear 0.4s linear;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body><div class="circle">✓</div></body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
class _SSORequestHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self) -> None:
|
||||||
|
parameters = parse_qs(urlparse(self.path).query)
|
||||||
|
|
||||||
|
if "loginToken" in parameters:
|
||||||
|
self.server._token = parameters["loginToken"][0] # type: ignore
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(_SUCCESS_HTML_PAGE.encode())
|
||||||
|
else:
|
||||||
|
self.send_error(400, "missing loginToken parameter")
|
||||||
|
|
||||||
|
self.close_connection = True
|
||||||
|
|
||||||
|
|
||||||
|
class SSOServer(HTTPServer):
|
||||||
|
"""Local HTTP server to retrieve a SSO login token.
|
||||||
|
|
||||||
|
Call `SSOServer.wait_for_token()` in a background task to start waiting
|
||||||
|
for a SSO login token from the Matrix homeserver.
|
||||||
|
|
||||||
|
Once the task is running, the user must open `SSOServer.url_to_open` in
|
||||||
|
their browser, where they will be able to complete the login process.
|
||||||
|
Once they are done, the homeserver will call us back with a login token
|
||||||
|
and the `SSOServer.wait_for_token()` task will return.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, for_homeserver: str) -> None:
|
||||||
|
self.for_homeserver: str = for_homeserver
|
||||||
|
self._token: str = ""
|
||||||
|
|
||||||
|
# Pick the first available port
|
||||||
|
super().__init__(("127.0.0.1", 0), _SSORequestHandler)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_to_open(self) -> str:
|
||||||
|
"""URL for the user to open in their browser, to do the SSO process."""
|
||||||
|
|
||||||
|
return "%s/_matrix/client/r0/login/sso/redirect?redirectUrl=%s" % (
|
||||||
|
self.for_homeserver,
|
||||||
|
quote(f"http://{self.server_address[0]}:{self.server_port}/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_token(self) -> str:
|
||||||
|
"""Wait until the homeserver gives us a login token and return it."""
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
while not self._token:
|
||||||
|
await loop.run_in_executor(None, self.handle_request)
|
||||||
|
|
||||||
|
return self._token
|
|
@ -7,8 +7,12 @@ import "../../Base"
|
||||||
HSwipeView {
|
HSwipeView {
|
||||||
id: swipeView
|
id: swipeView
|
||||||
clip: true
|
clip: true
|
||||||
interactive: currentIndex !== 0 || signIn.serverUrl
|
interactive: serverBrowser.acceptedUrl
|
||||||
onCurrentItemChanged: if (currentIndex === 0) serverBrowser.takeFocus()
|
onCurrentItemChanged:
|
||||||
|
currentIndex === 0 ?
|
||||||
|
serverBrowser.takeFocus() :
|
||||||
|
signInLoader.takeFocus()
|
||||||
|
|
||||||
Component.onCompleted: serverBrowser.takeFocus()
|
Component.onCompleted: serverBrowser.takeFocus()
|
||||||
|
|
||||||
HPage {
|
HPage {
|
||||||
|
@ -26,6 +30,8 @@ HSwipeView {
|
||||||
HPage {
|
HPage {
|
||||||
id: tabPage
|
id: tabPage
|
||||||
|
|
||||||
|
enabled: swipeView.currentItem === this
|
||||||
|
|
||||||
HTabbedBox {
|
HTabbedBox {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Math.min(implicitWidth, tabPage.availableWidth)
|
width: Math.min(implicitWidth, tabPage.availableWidth)
|
||||||
|
@ -37,11 +43,32 @@ HSwipeView {
|
||||||
HTabButton { text: qsTr("Reset") }
|
HTabButton { text: qsTr("Reset") }
|
||||||
}
|
}
|
||||||
|
|
||||||
SignIn {
|
HLoader {
|
||||||
id: signIn
|
id: signInLoader
|
||||||
serverUrl: serverBrowser.acceptedUrl
|
|
||||||
displayUrl: serverBrowser.acceptedUserUrl
|
readonly property Component signInPassword: SignInPassword {
|
||||||
onExitRequested: swipeView.currentIndex = 0
|
serverUrl: serverBrowser.acceptedUrl
|
||||||
|
displayUrl: serverBrowser.acceptedUserUrl
|
||||||
|
onExitRequested: swipeView.currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Component signInSso: SignInSso {
|
||||||
|
serverUrl: serverBrowser.acceptedUrl
|
||||||
|
displayUrl: serverBrowser.acceptedUserUrl
|
||||||
|
onExitRequested: swipeView.currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeFocus() { if (item) item.takeFocus() }
|
||||||
|
|
||||||
|
sourceComponent:
|
||||||
|
serverBrowser.loginFlows.includes("m.login.password") ?
|
||||||
|
signInPassword :
|
||||||
|
|
||||||
|
serverBrowser.loginFlows.includes("m.login.sso") &&
|
||||||
|
serverBrowser.loginFlows.includes("m.login.token") ?
|
||||||
|
signInSso :
|
||||||
|
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
Register {}
|
Register {}
|
||||||
|
|
|
@ -11,7 +11,7 @@ HBox {
|
||||||
|
|
||||||
property string acceptedUserUrl: ""
|
property string acceptedUserUrl: ""
|
||||||
property string acceptedUrl: ""
|
property string acceptedUrl: ""
|
||||||
property var loginFlows: ["m.login.password"]
|
property var loginFlows: []
|
||||||
|
|
||||||
property string saveName: "serverBrowser"
|
property string saveName: "serverBrowser"
|
||||||
property var saveProperties: ["acceptedUserUrl"]
|
property var saveProperties: ["acceptedUserUrl"]
|
||||||
|
@ -30,8 +30,20 @@ HBox {
|
||||||
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
|
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
|
||||||
connectTimeout.stop()
|
connectTimeout.stop()
|
||||||
errorMessage.text = ""
|
errorMessage.text = ""
|
||||||
|
connectFuture = null
|
||||||
|
|
||||||
|
if (! (
|
||||||
|
flows.includes("m.login.password") ||
|
||||||
|
(
|
||||||
|
flows.includes("m.login.sso") &&
|
||||||
|
flows.includes("m.login.token")
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
errorMessage.text =
|
||||||
|
qsTr("No supported sign-in method for this homeserver.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
connectFuture = null
|
|
||||||
acceptedUrl = url
|
acceptedUrl = url
|
||||||
acceptedUserUrl = args[0]
|
acceptedUserUrl = args[0]
|
||||||
loginFlows = flows
|
loginFlows = flows
|
||||||
|
|
|
@ -12,66 +12,42 @@ HFlickableColumnPage {
|
||||||
|
|
||||||
property string serverUrl
|
property string serverUrl
|
||||||
property string displayUrl: serverUrl
|
property string displayUrl: serverUrl
|
||||||
property var loginFuture: null
|
|
||||||
|
|
||||||
signal exitRequested()
|
property var loginFuture: null
|
||||||
|
|
||||||
readonly property int security:
|
readonly property int security:
|
||||||
serverUrl.startsWith("https://") ?
|
serverUrl.startsWith("https://") ?
|
||||||
SignIn.Security.Secure :
|
SignInBase.Security.Secure :
|
||||||
|
|
||||||
["//localhost", "//127.0.0.1", "//:1"].includes(
|
["//localhost", "//127.0.0.1", "//:1"].includes(
|
||||||
serverUrl.split(":")[1],
|
serverUrl.split(":")[1],
|
||||||
) ?
|
) ?
|
||||||
SignIn.Security.LocalHttp :
|
SignInBase.Security.LocalHttp :
|
||||||
|
|
||||||
SignIn.Security.Insecure
|
SignInBase.Security.Insecure
|
||||||
|
|
||||||
function takeFocus() { idField.item.forceActiveFocus() }
|
default property alias innerData: inner.data
|
||||||
|
readonly property alias rememberAccount: rememberAccount
|
||||||
|
readonly property alias errorMessage: errorMessage
|
||||||
|
readonly property alias applyButton: applyButton
|
||||||
|
|
||||||
function signIn() {
|
signal exitRequested()
|
||||||
if (page.loginFuture) page.loginFuture.cancel()
|
|
||||||
|
|
||||||
|
function finishSignIn(receivedUserId) {
|
||||||
errorMessage.text = ""
|
errorMessage.text = ""
|
||||||
|
page.loginFuture = null
|
||||||
|
|
||||||
const args = [
|
py.callCoro(
|
||||||
idField.item.text.trim(), passwordField.item.text,
|
rememberAccount.checked ?
|
||||||
undefined, page.serverUrl,
|
"saved_accounts.add":
|
||||||
]
|
"saved_accounts.delete",
|
||||||
|
|
||||||
page.loginFuture = py.callCoro("login_client", args, userId => {
|
[receivedUserId]
|
||||||
errorMessage.text = ""
|
)
|
||||||
page.loginFuture = null
|
|
||||||
|
|
||||||
print(rememberAccount.checked)
|
pageLoader.showPage(
|
||||||
py.callCoro(
|
"AccountSettings/AccountSettings", {userId: receivedUserId}
|
||||||
rememberAccount.checked ?
|
)
|
||||||
"saved_accounts.add": "saved_accounts.delete",
|
|
||||||
|
|
||||||
[userId]
|
|
||||||
)
|
|
||||||
|
|
||||||
pageLoader.showPage(
|
|
||||||
"AccountSettings/AccountSettings", {userId}
|
|
||||||
)
|
|
||||||
|
|
||||||
}, (type, args, error, traceback, uuid) => {
|
|
||||||
page.loginFuture = null
|
|
||||||
|
|
||||||
let txt = qsTr(
|
|
||||||
"Invalid request, login type or unknown error: %1",
|
|
||||||
).arg(type)
|
|
||||||
|
|
||||||
type === "MatrixForbidden" ?
|
|
||||||
txt = qsTr("Invalid username or password") :
|
|
||||||
|
|
||||||
type === "MatrixUserDeactivated" ?
|
|
||||||
txt = qsTr("This account was deactivated") :
|
|
||||||
|
|
||||||
utils.showError(type, traceback, uuid)
|
|
||||||
|
|
||||||
errorMessage.text = txt
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
|
@ -91,12 +67,11 @@ HFlickableColumnPage {
|
||||||
footer: AutoDirectionLayout {
|
footer: AutoDirectionLayout {
|
||||||
ApplyButton {
|
ApplyButton {
|
||||||
id: applyButton
|
id: applyButton
|
||||||
enabled: idField.item.text.trim() && passwordField.item.text
|
|
||||||
text: qsTr("Sign in")
|
text: qsTr("Sign in")
|
||||||
icon.name: "sign-in"
|
icon.name: "sign-in"
|
||||||
loading: page.loginFuture !== null
|
loading: page.loginFuture !== null
|
||||||
disableWhileLoading: false
|
disableWhileLoading: false
|
||||||
onClicked: page.signIn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CancelButton {
|
CancelButton {
|
||||||
|
@ -104,27 +79,28 @@ HFlickableColumnPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyboardAccept: if (applyButton.enabled) page.signIn()
|
onKeyboardAccept: if (applyButton.enabled) applyButton.clicked()
|
||||||
onKeyboardCancel: page.cancel()
|
onKeyboardCancel: page.cancel()
|
||||||
|
Component.onDestruction: if (loginFuture) loginFuture.cancel()
|
||||||
|
|
||||||
HButton {
|
HButton {
|
||||||
icon.name: "sign-in-" + (
|
icon.name: "sign-in-" + (
|
||||||
page.security === SignIn.Security.Insecure ? "insecure" :
|
page.security === SignInBase.Security.Insecure ? "insecure" :
|
||||||
page.security === SignIn.Security.LocalHttp ? "local-http" :
|
page.security === SignInBase.Security.LocalHttp ? "local-http" :
|
||||||
"secure"
|
"secure"
|
||||||
)
|
)
|
||||||
|
|
||||||
icon.color:
|
icon.color:
|
||||||
page.security === SignIn.Security.Insecure ?
|
page.security === SignInBase.Security.Insecure ?
|
||||||
theme.colors.negativeBackground :
|
theme.colors.negativeBackground :
|
||||||
|
|
||||||
page.security === SignIn.Security.LocalHttp ?
|
page.security === SignInBase.Security.LocalHttp ?
|
||||||
theme.colors.middleBackground :
|
theme.colors.middleBackground :
|
||||||
|
|
||||||
theme.colors.positiveBackground
|
theme.colors.positiveBackground
|
||||||
|
|
||||||
text:
|
text:
|
||||||
page.security === SignIn.Security.Insecure ?
|
page.security === SignInBase.Security.Insecure ?
|
||||||
page.serverUrl :
|
page.serverUrl :
|
||||||
page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "")
|
page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "")
|
||||||
|
|
||||||
|
@ -134,27 +110,9 @@ HFlickableColumnPage {
|
||||||
Layout.maximumWidth: parent.width
|
Layout.maximumWidth: parent.width
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
HColumnLayout {
|
||||||
id: idField
|
id: inner
|
||||||
label.text: qsTr("Username:")
|
spacing: page.column.spacing
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
HTextField {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HLabeledItem {
|
|
||||||
id: passwordField
|
|
||||||
label.text: qsTr("Password:")
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
HTextField {
|
|
||||||
width: parent.width
|
|
||||||
echoMode: HTextField.Password
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HCheckBox {
|
HCheckBox {
|
69
src/gui/Pages/AddAccount/SignInPassword.qml
Normal file
69
src/gui/Pages/AddAccount/SignInPassword.qml
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../../Base"
|
||||||
|
import "../../Base/Buttons"
|
||||||
|
|
||||||
|
SignInBase {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
function takeFocus() { idField.item.forceActiveFocus() }
|
||||||
|
|
||||||
|
function signIn() {
|
||||||
|
if (page.loginFuture) page.loginFuture.cancel()
|
||||||
|
|
||||||
|
errorMessage.text = ""
|
||||||
|
|
||||||
|
page.loginFuture = py.callCoro(
|
||||||
|
"password_auth",
|
||||||
|
[idField.item.text.trim(), passField.item.text, page.serverUrl],
|
||||||
|
page.finishSignIn,
|
||||||
|
|
||||||
|
(type, args, error, traceback, uuid) => {
|
||||||
|
page.loginFuture = null
|
||||||
|
|
||||||
|
let txt = qsTr(
|
||||||
|
"Invalid request, login type or unknown error: %1",
|
||||||
|
).arg(type)
|
||||||
|
|
||||||
|
type === "MatrixForbidden" ?
|
||||||
|
txt = qsTr("Invalid username or password") :
|
||||||
|
|
||||||
|
type === "MatrixUserDeactivated" ?
|
||||||
|
txt = qsTr("This account was deactivated") :
|
||||||
|
|
||||||
|
utils.showError(type, traceback, uuid)
|
||||||
|
|
||||||
|
page.errorMessage.text = txt
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
applyButton.enabled: idField.item.text.trim() && passField.item.text
|
||||||
|
applyButton.onClicked: page.signIn()
|
||||||
|
|
||||||
|
HLabeledItem {
|
||||||
|
id: idField
|
||||||
|
label.text: qsTr("Username:")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
HTextField {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HLabeledItem {
|
||||||
|
id: passField
|
||||||
|
label.text: qsTr("Password:")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
HTextField {
|
||||||
|
width: parent.width
|
||||||
|
echoMode: HTextField.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
src/gui/Pages/AddAccount/SignInSso.qml
Normal file
55
src/gui/Pages/AddAccount/SignInSso.qml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../../Base"
|
||||||
|
import "../../Base/Buttons"
|
||||||
|
|
||||||
|
SignInBase {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
function takeFocus() { urlField.forceActiveFocus() }
|
||||||
|
|
||||||
|
function startSignIn() {
|
||||||
|
errorMessage.text = ""
|
||||||
|
|
||||||
|
page.loginFuture = py.callCoro("start_sso_auth", [serverUrl], url => {
|
||||||
|
urlField.text = url
|
||||||
|
urlField.cursorPosition = 0
|
||||||
|
|
||||||
|
Qt.openUrlExternally(url)
|
||||||
|
|
||||||
|
page.loginFuture = py.callCoro("continue_sso_auth", [], userId => {
|
||||||
|
page.loginFuture = null
|
||||||
|
page.finishSignIn(userId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
applyButton.text: qsTr("Waiting")
|
||||||
|
applyButton.loading: true
|
||||||
|
Component.onCompleted: page.startSignIn()
|
||||||
|
|
||||||
|
HLabel {
|
||||||
|
wrapMode: HLabel.Wrap
|
||||||
|
text: qsTr(
|
||||||
|
"Complete the single sign-on process in your web browser to " +
|
||||||
|
"continue.\n\n" +
|
||||||
|
"If no page appeared, you can also manually open this address:"
|
||||||
|
)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
HTextArea {
|
||||||
|
id: urlField
|
||||||
|
width: parent.width
|
||||||
|
readOnly: true
|
||||||
|
radius: 0
|
||||||
|
wrapMode: HTextArea.WrapAnywhere
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,6 +86,7 @@ HMxcImage {
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
|
||||||
onTapped: {
|
onTapped: {
|
||||||
|
print(loader.mediaUrl, loader.singleMediaInfo.media_http_url)
|
||||||
if (eventList.selectedCount) {
|
if (eventList.selectedCount) {
|
||||||
eventDelegate.toggleChecked()
|
eventDelegate.toggleChecked()
|
||||||
return
|
return
|
||||||
|
|
|
@ -10,6 +10,7 @@ import "MainPane"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: mainUI
|
id: mainUI
|
||||||
|
enabled: ! window.anyPopup
|
||||||
|
|
||||||
property bool accountsPresent:
|
property bool accountsPresent:
|
||||||
ModelStore.get("accounts").count > 0 || py.startupAnyAccountsSaved
|
ModelStore.get("accounts").count > 0 || py.startupAnyAccountsSaved
|
||||||
|
|
Loading…
Reference in New Issue
Block a user