Disable current push device on logout

This commit is contained in:
SimpleTest 2026-07-23 20:37:38 +03:00
parent d18470f77a
commit e7cfddb587
3 changed files with 89 additions and 3 deletions

View File

@ -192,6 +192,26 @@ document.addEventListener("submit", event => {
const action = new URL(form.action, window.location.origin)
if (action.origin === window.location.origin && action.pathname === "/users/log-out") {
const deviceId = window.localStorage.getItem("wnh.push.server-device-id")
if (deviceId) {
const input = document.createElement("input")
input.type = "hidden"
input.name = "push_device_id"
input.value = deviceId
form.append(input)
window.localStorage.removeItem("wnh.push.server-device-id")
}
if (window.WhoNeedHelpAndroid?.postMessage) {
window.WhoNeedHelpAndroid.postMessage(JSON.stringify({action: "disable_push"}))
} else if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then(registration => registration.pushManager.getSubscription())
.then(subscription => subscription?.unsubscribe())
.catch(error => console.warn("Browser push unsubscribe on logout failed", error))
}
window.WhoNeedHelpAndroid?.postMessage?.(JSON.stringify({action: "google_sign_out"}))
}
})

View File

@ -3,7 +3,8 @@ defmodule WhoNeedHelpWeb.UserSessionController do
require Logger
alias WhoNeedHelp.{Accounts, GoogleAuth}
alias WhoNeedHelp.{Accounts, GoogleAuth, Notifications}
alias WhoNeedHelp.Accounts.Scope
alias WhoNeedHelp.Trust.RateLimiter
alias WhoNeedHelpWeb.{GoogleAuthPending, UserAuth}
@ -125,12 +126,26 @@ defmodule WhoNeedHelpWeb.UserSessionController do
|> render(:new, form: Phoenix.Component.to_form(%{}, as: "user"))
end
def delete(conn, _params) do
def delete(conn, params) do
conn
|> maybe_disable_logout_push_device(params)
|> put_flash(:info, gettext("Logged out successfully."))
|> UserAuth.log_out_user()
end
defp maybe_disable_logout_push_device(
%{assigns: %{current_scope: %Scope{user: %Accounts.User{}} = scope}} = conn,
%{"push_device_id" => device_id}
)
when is_binary(device_id) do
# The context scopes the lookup to the signed-in user. Missing, malformed, or
# another user's device IDs must never prevent the session from being closed.
_result = Notifications.disable_device(scope, device_id)
conn
end
defp maybe_disable_logout_push_device(conn, _params), do: conn
defp invalid_password_response(conn, user_params) do
# Keep the response identical for unknown accounts and incorrect passwords.
conn

View File

@ -3,7 +3,7 @@ defmodule WhoNeedHelpWeb.UserSessionControllerTest do
import WhoNeedHelp.AccountsFixtures
import Swoosh.TestAssertions
alias WhoNeedHelp.Accounts
alias WhoNeedHelp.{Accounts, Notifications}
setup do
%{unconfirmed_user: unconfirmed_user_fixture(), user: user_fixture()}
@ -273,5 +273,56 @@ defmodule WhoNeedHelpWeb.UserSessionControllerTest do
refute get_session(conn, :user_token)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
end
test "disables only the current browser push device", %{conn: conn, user: user} do
scope = Accounts.Scope.for_user(user)
{:ok, current_device} = Notifications.register_device(scope, push_device_attrs())
{:ok, remaining_device} = Notifications.register_device(scope, push_device_attrs())
conn =
conn
|> log_in_user(user)
|> delete(~p"/users/log-out", %{"push_device_id" => current_device.id})
assert redirected_to(conn) == ~p"/"
assert Enum.map(Notifications.list_devices(scope), & &1.id) == [remaining_device.id]
end
test "cannot disable another user's push device during logout", %{conn: conn, user: user} do
other_user = user_fixture()
other_scope = Accounts.Scope.for_user(other_user)
{:ok, other_device} = Notifications.register_device(other_scope, push_device_attrs())
conn =
conn
|> log_in_user(user)
|> delete(~p"/users/log-out", %{"push_device_id" => other_device.id})
assert redirected_to(conn) == ~p"/"
assert [%{id: device_id}] = Notifications.list_devices(other_scope)
assert device_id == other_device.id
end
test "malformed push device ID does not prevent logout", %{conn: conn, user: user} do
conn =
conn
|> log_in_user(user)
|> delete(~p"/users/log-out", %{"push_device_id" => "not-a-device-id"})
assert redirected_to(conn) == ~p"/"
refute get_session(conn, :user_token)
end
end
defp push_device_attrs do
%{
"platform" => "web",
"provider" => "web_push",
"token" => "https://push.example/subscriptions/#{Ecto.UUID.generate()}",
"installation_id" => Ecto.UUID.generate(),
"p256dh" => "test-public-key",
"auth_secret" => "test-auth-secret",
"device_label" => "Test browser"
}
end
end