Add sessions in settings

This commit is contained in:
Zoe Roux 2026-03-26 19:09:30 +01:00
parent cd7c350b20
commit 0435ffc655
No known key found for this signature in database
4 changed files with 91 additions and 6 deletions

View File

@ -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 {

View File

@ -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}}.",

View File

@ -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 = () => {
<GeneralSettings />
{account && <PlaybackSettings />}
{account && <AccountSettings />}
{account && <SessionsSettings />}
{account && <OidcSettings />}
<About />
</ScrollView>

View File

@ -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<string | null>(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 (
<SettingsContainer title={t("settings.sessions.label")}>
{error && <P className="mx-6 text-red-500">{error}</P>}
{items.map((session) => (
<Preference
key={session.id}
icon={Devices}
label={session.device}
description={
session.current
? t("settings.sessions.current")
: t("settings.sessions.description", {
createdDate: session.createdDate.toLocaleString(),
lastUsed: session.lastUsed.toLocaleString(),
})
}
>
<Button
text={t("settings.sessions.revoke")}
disabled={isPending}
onPress={async () => {
setError(null);
try {
await revokeSession(session.id);
} catch (e) {
setError((e as KyooError).message);
}
}}
/>
</Preference>
))}
</SettingsContainer>
);
};
SessionsSettings.query = (): QueryIdentifier<Session[]> => ({
path: ["auth", "users", "me", "sessions"],
parser: z.array(Session),
});
const Session = z.object({
id: z.string(),
createdDate: z.coerce.date(),
lastUsed: z.coerce.date(),
device: z.string(),
current: z.boolean(),
});
type Session = z.infer<typeof Session>;