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) => (
+
+
+ ))}
+
+ );
+};
+
+SessionsSettings.query = (): QueryIdentifier => ({
+ 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;