mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-28 04:17:50 -04:00
Add sessions in settings
This commit is contained in:
parent
cd7c350b20
commit
0435ffc655
@ -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 {
|
||||
|
||||
@ -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}}.",
|
||||
|
||||
@ -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>
|
||||
|
||||
70
front/src/ui/settings/sessions.tsx
Normal file
70
front/src/ui/settings/sessions.tsx
Normal 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>;
|
||||
Loading…
x
Reference in New Issue
Block a user