Harden deployment, authentication, firewall and media storage failure paths

This commit is contained in:
rope 2026-09-14 11:50:22 +01:00
parent 64ede7bd05
commit 7145a35927
35 changed files with 2529 additions and 499 deletions

View file

@ -0,0 +1,226 @@
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Services.Mpris
import QtQuick
import Qt5Compat.GraphicalEffects
// Shared appearance; authentication belongs to the greeter or session lock.
Item {
id: view
required property var theme
property bool busy: false
property string errorMessage: ""
signal submitted(string password)
Image {
id: wall
anchors.fill: parent
source: "file://" + view.theme.wallImage
// cache:false — soft reload (qs ipc reload) keeps the process
// alive, and the QML pixmap cache is keyed by URL, so a new
// wallpaper behind the same path never shows without this.
cache: false
fillMode: Image.PreserveAspectCrop
visible: false
}
FastBlur {
anchors.fill: wall
source: wall
radius: 20
}
// Lighter blur means less natural contrast, so the scrim
// is a left-weighted gradient instead of a flat 0.4 wash:
// dark under the text column, near-clear on the right.
Rectangle {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#b0000000" }
GradientStop { position: 0.55; color: "#59000000" }
GradientStop { position: 1.0; color: "#33000000" }
}
}
SystemClock {
id: lockClock
precision: SystemClock.Seconds
}
// Lower-left, not centred: clock/date/password stack left
// aligned against a 10% margin, sitting below the middle.
Column {
anchors.left: parent.left
anchors.leftMargin: Math.round(parent.width * 0.10)
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: Math.round(parent.height * 0.14)
spacing: 6
Text {
text: lockClock.date.toLocaleTimeString(Qt.locale(), "HH:mm")
color: view.theme.base05
font.family: view.theme.fontFamily
font.pixelSize: 104
font.weight: Font.DemiBold
}
Text {
text: lockClock.date.toLocaleDateString(Qt.locale(), "dddd, d MMMM")
color: view.theme.base04
font.family: view.theme.fontFamily
font.pixelSize: 18
}
Item { width: 1; height: 14 }
// Power. UPower.displayDevice is the same DBus source
// the bar reads — no polling, no /sys parsing.
Row {
id: lockBatt
spacing: 8
property var dev: UPower.displayDevice
property int level: dev && dev.ready ? Math.round(dev.percentage * 100) : 0
property bool charging: dev ? dev.state === UPowerDeviceState.Charging : false
property string remaining: {
if (!dev) return "";
let secs = charging ? dev.timeToFull : dev.timeToEmpty;
if (!secs || secs <= 0) return "";
return Math.floor(secs / 3600) + "h " + Math.round((secs % 3600) / 60) + "m";
}
visible: dev && dev.ready
Text {
anchors.verticalCenter: parent.verticalCenter
text: lockBatt.charging ? "battery-charging"
: lockBatt.level >= 75 ? "battery-full"
: lockBatt.level >= 50 ? "battery-medium"
: lockBatt.level >= 20 ? "battery-low"
: "battery"
font.family: view.theme.iconFont
font.pixelSize: 17
color: lockBatt.level <= 15 ? view.theme.err
: lockBatt.level <= 30 ? view.theme.warn : view.theme.base04
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: lockBatt.level + "%" + (lockBatt.remaining === "" ? ""
: " · " + lockBatt.remaining + (lockBatt.charging ? " to full" : " left"))
color: view.theme.base04
font.family: view.theme.fontFamily
font.pixelSize: 15
}
}
// Now playing. Mpris is a global service, so this shows
// whatever kept playing into the lock — nothing is woken.
Row {
id: lockMedia
spacing: 8
property var player: Mpris.players.values.filter(p => p.trackTitle)[0]
visible: !!player
Text {
anchors.verticalCenter: parent.verticalCenter
text: "music"
font.family: view.theme.iconFont
font.pixelSize: 17
color: view.theme.base04
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: 380
elide: Text.ElideRight
text: lockMedia.player
? lockMedia.player.trackTitle + (lockMedia.player.trackArtist
? " · " + lockMedia.player.trackArtist : "")
: ""
color: view.theme.base04
font.family: view.theme.fontFamily
font.pixelSize: 15
}
}
Item { width: 1; height: 14 }
// Password pill: dots for typed chars; border tracks
// state (accent while checking, red on failure).
Rectangle {
width: 300
height: 46
radius: view.theme.radiusCard
color: view.theme.cardBg
border.width: view.theme.borderWidth
border.color: view.busy ? view.theme.accent
: (view.errorMessage !== "") ? view.theme.err
: view.theme.base03
Behavior on border.color { ColorAnimation { duration: view.theme.animFade } }
Row {
anchors.centerIn: parent
spacing: 8
Repeater {
model: Math.min(pwInput.text.length, 24)
Rectangle {
width: 8; height: 8; radius: 4
color: view.theme.base05
}
}
}
Text {
anchors.centerIn: parent
visible: pwInput.text === "" && (view.errorMessage !== "")
text: view.errorMessage
color: view.theme.err
font.family: view.theme.fontFamily
font.pixelSize: 13
}
TextInput {
id: pwInput
anchors.fill: parent
opacity: 0
enabled: !view.busy
echoMode: TextInput.Password
focus: true
function go() {
view.submitted(text);
text = "";
}
// Explicit key handlers: accepted() doesn't
// fire reliably on the lock surface.
Keys.onReturnPressed: go()
Keys.onEnterPressed: go()
onAccepted: go()
Keys.onEscapePressed: text = ""
}
// Visible confirm button (after TextInput: on top)
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
width: 32; height: 32; radius: 16
color: confirmMa.containsMouse ? view.theme.base02 : "transparent"
Behavior on color { ColorAnimation { duration: view.theme.animFade } }
visible: pwInput.text.length > 0
Text {
anchors.centerIn: parent
text: "arrow-right"
font.family: view.theme.iconFont
font.pixelSize: 18
color: view.theme.accent
}
MouseArea {
id: confirmMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: pwInput.go()
}
}
}
}
Component.onCompleted: pwInput.forceActiveFocus()
}

View file

@ -0,0 +1,65 @@
import Quickshell
import Quickshell.Services.Greetd
import QtQuick
import QtQuick.Window
Window {
id: root
visible: true
visibility: Window.FullScreen
color: "black"
property string password: ""
property string errorMessage: ""
property bool busy: false
Component.onCompleted: Quickshell.watchFiles = false
function failed(message) {
password = "";
busy = false;
errorMessage = message;
}
Connections {
target: Greetd
function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) {
// This greeter supports the configured password-only PAM flow.
if (echoResponse || root.password === "") {
Greetd.cancelSession();
root.failed("Use a text console for this prompt");
return;
}
Greetd.respond(root.password);
root.password = "";
} else if (error) root.errorMessage = message;
}
function onAuthFailure(message) { root.failed("Authentication failed"); }
function onError(error) { root.failed("Login unavailable; try again"); }
function onReadyToLaunch() {
root.password = "";
// PAM/logind must know this is a graphical session before the
// compositor starts, including for the pre-suspend lock check.
Greetd.launch([Quickshell.env("FRED_SESSION_COMMAND")], [
"XDG_SESSION_TYPE=wayland",
"XDG_CURRENT_DESKTOP=Hyprland",
"XDG_SESSION_DESKTOP=Hyprland"
]);
}
}
AuthView {
anchors.fill: parent
theme: Theme
busy: root.busy
errorMessage: root.errorMessage
onSubmitted: password => {
if (root.busy || password === "") return;
if (!Greetd.available) {
root.failed("Login service unavailable");
return;
}
root.errorMessage = "";
root.password = password;
root.busy = true;
Greetd.createSession("fred");
}
}
}

View file

@ -0,0 +1,61 @@
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Pam
import QtQuick
Scope {
id: root
property string password: ""
property string errorMessage: ""
// Immutable configuration, independent of bar edits and reloads.
Component.onCompleted: Quickshell.watchFiles = false
function requestLock() {
Theme.wallFile.reload();
sessionLock.locked = true;
}
IpcHandler {
target: "lock"
function lock(): void { root.requestLock(); }
function secure(): bool { return sessionLock.secure; }
}
PamContext {
id: pam
config: "fred-lock"
onPamMessage: {
if (responseRequired) {
respond(root.password);
root.password = "";
}
}
onCompleted: result => {
root.password = "";
if (result === PamResult.Success) sessionLock.locked = false;
else root.errorMessage = "Authentication failed";
}
}
WlSessionLock {
id: sessionLock
// Start only on demand, but always relock after a process crash.
locked: true
WlSessionLockSurface {
color: "black"
AuthView {
anchors.fill: parent
theme: Theme
busy: pam.active
errorMessage: root.errorMessage
onSubmitted: password => {
if (pam.active || password === "") return;
root.errorMessage = "";
root.password = password;
if (!pam.start()) {
root.password = "";
root.errorMessage = "Authentication unavailable";
}
}
}
}
}
}