feat(chat): join users to rooms on sign-in

This commit is contained in:
Nathnael
2026-08-17 11:33:26 +00:00
parent 00bd1250ee
commit ab734aecc3
15 changed files with 366 additions and 114 deletions

View File

@@ -0,0 +1,18 @@
#!/bin/sh
# Renders /app/config.json from config.json.tmpl at container start.
#
# The homeserver URL and server_name differ per environment, and config.json is
# read by the browser rather than the build, so baking it into the image would
# mean one image per environment. Dropped into /docker-entrypoint.d, which the
# upstream nginx entrypoint runs (in lexical order) before starting nginx —
# no ENTRYPOINT override, so the image's own startup work still happens.
set -eu
: "${MATRIX_PUBLIC_BASEURL:?MATRIX_PUBLIC_BASEURL is required}"
: "${MATRIX_SERVER_NAME:?MATRIX_SERVER_NAME is required}"
: "${ELEMENT_PUBLIC_URL:?ELEMENT_PUBLIC_URL is required}"
envsubst '${MATRIX_PUBLIC_BASEURL} ${MATRIX_SERVER_NAME} ${ELEMENT_PUBLIC_URL}' \
< /app/config.json.tmpl > /app/config.json
echo "element-config: homeserver ${MATRIX_PUBLIC_BASEURL} (${MATRIX_SERVER_NAME})"

View File

@@ -5,5 +5,19 @@
# Pin the tag; never float on `latest`.
FROM ghcr.io/element-hq/element-web:v1.11.108
COPY config.json /app/config.json
COPY config.json.tmpl /app/config.json.tmpl
COPY sso.html /app/sso.html
# Replaces the upstream manifest, which names the app "Element" and advertises
# the Play/App Store builds under related_applications. Those apps cannot log
# in here — this deployment has no password login and no SSO provider, only the
# JWT handoff from freight-api — so pointing staff at them is a dead end.
COPY manifest.json /app/manifest.json
COPY 40-element-config.sh /docker-entrypoint.d/40-element-config.sh
# The image runs as uid 101 (nginx) but ships /app root-owned, so the startup
# hook could not write the rendered config without this.
USER root
RUN chmod +x /docker-entrypoint.d/40-element-config.sh \
&& touch /app/config.json \
&& chown nginx:nginx /app/config.json
USER nginx

View File

@@ -1,16 +1,17 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.edr.et",
"server_name": "matrix.edr.et"
"base_url": "${MATRIX_PUBLIC_BASEURL}",
"server_name": "${MATRIX_SERVER_NAME}"
}
},
"brand": "EDR Chat",
"permalink_prefix": "https://chat.edr.et",
"permalink_prefix": "${ELEMENT_PUBLIC_URL}",
"disable_guests": true,
"disable_3pid_login": true,
"disable_custom_urls": true,
"default_theme": "light",
"mobile_guide_toast": false,
"settingDefaults": {
"UIFeature.registration": false
}

View File

@@ -0,0 +1,12 @@
{
"name": "EDR Chat",
"short_name": "EDR Chat",
"display": "standalone",
"theme_color": "#0dbd8b",
"start_url": "index.html",
"icons": [
{ "src": "/vector-icons/150.png", "sizes": "150x150", "type": "image/png" },
{ "src": "/vector-icons/300.png", "sizes": "300x300", "type": "image/png" },
{ "src": "/vector-icons/1024.png", "sizes": "1024x1024", "type": "image/png" }
]
}

View File

@@ -1,16 +1,23 @@
<!doctype html>
<!--
Element only honours a `?loginToken=` on `/` if `mx_sso_hs_url` is already
in localStorage (element-web apps/web/src/Lifecycle.ts attemptTokenLogin,
key defined in apps/web/src/BasePlatform.ts). Normally that key is written
by Element itself at the start of an SSO redirect; freight-api's SSO
handoff skips that redirect (it already knows the homeserver), so this
page seeds the key by hand and forwards straight to the login-token URL.
Session handoff from freight-api into Element.
freight-api's chat-sso.service.ts links here as
https://chat.edr.et/sso.html?t=<login_token>&hs=<homeserver base_url>.
`hs` is passed rather than hardcoded so this file doesn't need to change if
MATRIX_PUBLIC_BASEURL ever does.
https://chat.edr.et/sso.html#hs=<homeserver>&t=<access_token>&u=<user_id>&d=<device_id>
— a fragment, not a query, so the token is never sent to a server and never
lands in an access log. location.replace() below drops this URL from history
as well, so the token does not survive the redirect.
The keys written here are the ones Element reads on startup
(element-web src/Lifecycle.ts getStoredSessionVars/getStoredToken): the token
is looked up in IndexedDB first and falls back to localStorage, which Element
then migrates into IndexedDB itself. A plaintext token is accepted —
tryDecryptToken returns a string token as-is, and only decrypts when it finds
an encrypted payload.
This replaced a ?loginToken= handoff: POST /_matrix/client/v1/login/get_token
is capped at one call per user per minute by a limiter hardcoded in Synapse,
so clicking Chat twice in a minute failed.
-->
<html lang="en">
<head>
@@ -19,17 +26,23 @@
</head>
<body>
<script>
var params = new URLSearchParams(window.location.search);
var token = params.get("t");
var params = new URLSearchParams(window.location.hash.slice(1));
var homeserver = params.get("hs");
if (token && homeserver) {
localStorage.setItem("mx_sso_hs_url", homeserver);
window.location.replace(
"/?loginToken=" + encodeURIComponent(token),
);
var token = params.get("t");
var userId = params.get("u");
var deviceId = params.get("d");
if (homeserver && token && userId && deviceId) {
localStorage.setItem("mx_hs_url", homeserver);
localStorage.setItem("mx_user_id", userId);
localStorage.setItem("mx_device_id", deviceId);
localStorage.setItem("mx_access_token", token);
localStorage.setItem("mx_has_access_token", "true");
localStorage.setItem("mx_is_guest", "false");
window.location.replace("/");
} else {
document.body.textContent =
"Missing sign-in token. Go back to the EDR backoffice and click Chat again.";
"Missing sign-in details. Go back to the EDR backoffice and click Chat again.";
}
</script>
</body>

View File

@@ -42,6 +42,24 @@ federation_domain_whitelist: []
enable_registration: false
encryption_enabled_by_default_for_room_type: "off"
# Turning rooms' encryption off above is not enough on its own: Element still
# bootstraps cross-signing on a user's first login, and from then on gates
# EVERY later login behind "Verify this device" (MatrixChat: crossSigningIsSetUp
# -> Views.COMPLETE_SECURITY). Nobody on this deployment can clear that gate —
# each SSO click is a brand-new device, so there is never a second verified
# device to accept the request, and resetting the identity needs UIA, which
# password_config.enabled: false makes impossible.
#
# This tells Element encryption is off here, so it skips the bootstrap
# (shouldSkipSetupEncryption) and the gate is never armed. Only helps accounts
# that have no cross-signing keys yet — anyone already bootstrapped keeps
# hitting the gate until their keys are cleared.
extra_well_known_client_content:
io.element.e2ee:
default: false
force_disable: true
secure_backup_required: false
# Employees authenticate via freight-api's SSO handoff, never a Matrix
# password prompt. This is the entire auth story for this deployment.
password_config: