fix: add logout endpoint and session cleanup

This commit is contained in:
2026-04-03 01:38:15 +03:00
parent 1e6f200433
commit 56aa822730
5 changed files with 58 additions and 3 deletions

View File

@@ -1,3 +1,4 @@
import { useMutation } from '@tanstack/react-query'
import {
ArrowLeft,
ArrowRight,
@@ -19,6 +20,7 @@ import { CommandPalette } from '@/components/command-palette'
import { FullPlayer } from '@/components/full-player'
import { PlayerBar } from '@/components/player-bar'
import { SettingsModal } from '@/components/settings-modal'
import { logout } from '@/lib/api'
import { usePlayerStore } from '@/stores/player-store'
import { useSessionStore } from '@/stores/session-store'
@@ -42,6 +44,13 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const [settingsOpen, setSettingsOpen] = useState(false)
const [userMenuOpen, setUserMenuOpen] = useState(false)
const [paletteOpen, setPaletteOpen] = useState(false)
const logoutMutation = useMutation({
mutationFn: logout,
onSettled: () => {
clearSession()
setUserMenuOpen(false)
},
})
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
@@ -99,7 +108,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
</button>
<button
className="flex w-full items-center justify-between border-t border-[#24314f] px-4 py-3 text-left text-sm text-slate-100 hover:bg-[#18233a]"
onClick={clearSession}
onClick={() => logoutMutation.mutate()}
type="button"
>
Выйти из аккаунта

View File

@@ -116,6 +116,12 @@ export async function login(username: string, password: string) {
})
}
export async function logout() {
return request<{ status: string }>('/api/auth/logout', {
method: 'POST',
})
}
export async function fetchHome() {
return request<HomePayload>('/api/home')
}

View File

@@ -92,7 +92,7 @@ export function LoginPage() {
{mutation.isError ? (
<div className="rounded-2xl border border-red-400/30 bg-red-500/10 px-4 py-3 text-sm text-red-200">
Could not reach the backend. Make sure the API is running on `http://localhost:4040`.
Could not reach the backend. Make sure the API is running on the same origin, or on `http://localhost:5050` in dev.
</div>
) : null}
</form>
@@ -111,4 +111,3 @@ function InfoCard({ label, value }: { label: string; value: string }) {
</div>
)
}

View File

@@ -47,6 +47,10 @@ func NewService(db *sql.DB, encryptionKey string) *Service {
}
func (s *Service) Login(ctx context.Context, username, password string) (Session, error) {
if err := s.cleanupExpiredSessions(ctx); err != nil {
return Session{}, fmt.Errorf("cleanup expired sessions: %w", err)
}
user, passwordHash, _, err := s.findUserByUsername(ctx, username)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@@ -107,6 +111,8 @@ func (s *Service) CurrentUserByToken(ctx context.Context, token string) (User, e
return User{}, ErrUnauthorized
}
_ = s.cleanupExpiredSessions(ctx)
user, err := s.findUserByToken(ctx, token)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@@ -118,6 +124,16 @@ func (s *Service) CurrentUserByToken(ctx context.Context, token string) (User, e
return user, nil
}
func (s *Service) Logout(ctx context.Context, token string) error {
if strings.TrimSpace(token) == "" {
return nil
}
if _, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE token = ?`, strings.TrimSpace(token)); err != nil {
return fmt.Errorf("delete session: %w", err)
}
return nil
}
func (s *Service) CurrentUserBySubsonicAuth(ctx context.Context, username, password, token, salt string) (User, error) {
if username == "" {
return User{}, ErrUnauthorized
@@ -262,6 +278,11 @@ func (s *Service) storeSubsonicSecret(ctx context.Context, userID, password stri
return err
}
func (s *Service) cleanupExpiredSessions(ctx context.Context) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE expires_at <= ?`, time.Now().UTC().Format(time.RFC3339))
return err
}
func EncryptSubsonicSecret(value, key string) (string, error) {
return encryptSecret(value, key)
}

View File

@@ -52,6 +52,7 @@ func NewRouter(cfg config.Config, database *sql.DB, scanService *scanner.Service
r.Route("/api", func(api chi.Router) {
api.Post("/auth/login", application.login)
api.Post("/auth/logout", application.logout)
api.Group(func(private chi.Router) {
private.Use(application.requireAuth)
@@ -142,6 +143,25 @@ func (a app) login(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, session)
}
func (a app) logout(w http.ResponseWriter, r *http.Request) {
token := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if token == "" {
var payload struct {
Token string `json:"token"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err == nil {
token = strings.TrimSpace(payload.Token)
}
}
if err := a.auth.Logout(r.Context(), token); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "logout failed"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
}
func (a app) me(w http.ResponseWriter, r *http.Request) {
user := currentUserFromContext(r)
writeJSON(w, http.StatusOK, user)