diff --git a/auth/sessions.go b/auth/sessions.go index 97cbc257..c9c2737e 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -34,6 +34,11 @@ type SessionWToken struct { Token string `json:"token" example:"lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="` } +type SessionWCurrent struct { + Session + Current bool `json:"current"` +} + func MapSession(ses *dbc.Session) Session { dev := ses.Device if ses.Device != nil { @@ -143,7 +148,7 @@ func (h *Handler) createSession(c *echo.Context, user *User) error { // @Tags sessions // @Produce json // @Security Jwt -// @Success 200 {array} Session +// @Success 200 {array} SessionWCurrent // @Failure 401 {object} KError "Missing jwt token" // @Failure 403 {object} KError "Invalid jwt token (or expired)" // @Router /sessions [get] @@ -167,9 +172,14 @@ func (h *Handler) ListMySessions(c *echo.Context) error { return err } - ret := make([]Session, 0, len(dbSessions)) + sid, _ := GetCurrentSessionId(c) + + ret := make([]SessionWCurrent, 0, len(dbSessions)) for _, ses := range dbSessions { - ret = append(ret, MapSession(&ses)) + ret = append(ret, SessionWCurrent{ + Session: MapSession(&ses), + Current: ses.Id == sid, + }) } return c.JSON(http.StatusOK, ret) @@ -199,9 +209,6 @@ func (h *Handler) ListUserSessions(c *echo.Context) error { Id: uid, Username: id, }) - if err != nil { - return err - } if err == pgx.ErrNoRows { return echo.NewHTTPError(http.StatusNotFound, "No user found with id or username") } else if err != nil { diff --git a/front/public/translations/en.json b/front/public/translations/en.json index cf878f1a..6b9c40ba 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -184,6 +184,12 @@ "newPassword": "New password" } }, + "sessions": { + "label": "Sessions", + "description": "Created {{createdDate}} - Last used {{lastUsed}}", + "current": "Current session", + "revoke": "Revoke" + }, "oidc": { "label": "Linked accounts", "connected": "Connected as {{username}}.", diff --git a/front/src/ui/settings/index.tsx b/front/src/ui/settings/index.tsx index d73fc2f2..76c5e7d1 100644 --- a/front/src/ui/settings/index.tsx +++ b/front/src/ui/settings/index.tsx @@ -4,6 +4,7 @@ import { AccountSettings } from "./account"; import { About, GeneralSettings } from "./general"; import { OidcSettings } from "./oidc"; import { PlaybackSettings } from "./playback"; +import { SessionsSettings } from "./sessions"; export const SettingsPage = () => { const account = useAccount(); @@ -12,6 +13,7 @@ export const SettingsPage = () => { {account && } {account && } + {account && } {account && } diff --git a/front/src/ui/settings/sessions.tsx b/front/src/ui/settings/sessions.tsx new file mode 100644 index 00000000..96162604 --- /dev/null +++ b/front/src/ui/settings/sessions.tsx @@ -0,0 +1,70 @@ +import Devices from "@material-symbols/svg-400/outlined/devices.svg"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod/v4"; +import type { KyooError } from "~/models"; +import { Button, P } from "~/primitives"; +import { type QueryIdentifier, useFetch, useMutation } from "~/query"; +import { Preference, SettingsContainer } from "./base"; + +export const SessionsSettings = () => { + const { t } = useTranslation(); + const [error, setError] = useState(null); + const { data: sessions } = useFetch(SessionsSettings.query()); + const items = sessions ?? []; + const { mutateAsync: revokeSession, isPending } = useMutation({ + method: "DELETE", + compute: (id: string) => ({ + path: ["auth", "sessions", id], + }), + invalidate: ["auth", "users", "me", "sessions"], + }); + + return ( + + {error &&

{error}

} + {items.map((session) => ( + +