Add GET /sessions

This commit is contained in:
Zoe Roux 2026-03-22 20:53:35 +01:00
parent a70431b460
commit bf925d8d70
No known key found for this signature in database
6 changed files with 169 additions and 15 deletions

View File

@ -1,8 +1,6 @@
module github.com/zoriya/kyoo/keibi
go 1.25.0
toolchain go1.26.0
go 1.26.0
require (
github.com/alexedwards/argon2id v1.0.0
@ -81,6 +79,7 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mileusna/useragent v1.3.5
github.com/swaggo/files/v2 v2.0.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0

View File

@ -130,6 +130,8 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=

View File

@ -346,8 +346,10 @@ func main() {
g.POST("/users", h.Register)
g.POST("/sessions", h.Login)
r.GET("/sessions", h.ListMySessions)
r.DELETE("/sessions", h.Logout)
r.DELETE("/sessions/:id", h.Logout)
r.GET("/users/:id/sessions", h.ListUserSessions)
r.GET("/keys", h.ListApiKey)
r.POST("/keys", h.CreateApiKey)

View File

@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/base64"
"net/http"
"strings"
"time"
"github.com/alexedwards/argon2id"
@ -12,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/labstack/echo/v5"
"github.com/mileusna/useragent"
"github.com/zoriya/kyoo/keibi/dbc"
)
@ -32,23 +34,30 @@ type SessionWToken struct {
}
func MapSession(ses *dbc.Session) Session {
dev := ses.Device
if ses.Device != nil {
ua := useragent.Parse(*ses.Device)
uae := ([]string{ua.Name})
if ua.Device != "" {
uae = append(uae, ua.Device)
}
if ua.OS != "" {
uae = append(uae, ua.OS)
}
dev = new(strings.Join(uae, " - "))
}
return Session{
Id: ses.Id,
CreatedDate: ses.CreatedDate,
LastUsed: ses.LastUsed,
Device: ses.Device,
Device: dev,
}
}
func MapSessionToken(ses *dbc.Session) SessionWToken {
return SessionWToken{
Session: Session{
Id: ses.Id,
CreatedDate: ses.CreatedDate,
LastUsed: ses.LastUsed,
Device: ses.Device,
},
Token: ses.Token,
Session: MapSession(ses),
Token: ses.Token,
}
}
@ -128,6 +137,87 @@ func (h *Handler) createSession(c *echo.Context, user *User) error {
return c.JSON(201, MapSessionToken(&session))
}
// @Summary List my sessions
// @Description List all active sessions for the currently connected user
// @Tags sessions
// @Produce json
// @Security Jwt
// @Success 200 {array} Session
// @Failure 401 {object} KError "Missing jwt token"
// @Failure 403 {object} KError "Invalid jwt token (or expired)"
// @Router /sessions [get]
func (h *Handler) ListMySessions(c *echo.Context) error {
ctx := c.Request().Context()
uid, err := GetCurrentUserId(c)
if err != nil {
return err
}
users, err := h.db.GetUser(ctx, dbc.GetUserParams{
UseId: true,
Id: uid,
})
if err != nil {
return err
}
dbSessions, err := h.db.GetUserSessions(ctx, users[0].User.Pk)
if err != nil {
return err
}
ret := make([]Session, 0, len(dbSessions))
for _, ses := range dbSessions {
ret = append(ret, MapSession(&ses))
}
return c.JSON(http.StatusOK, ret)
}
// @Summary List user sessions
// @Description List all active sessions for a user. Listing someone else's sessions requires users.read.
// @Tags sessions
// @Produce json
// @Security Jwt
// @Param id path string true "The id or username of the user" Example(e05089d6-9179-4b5b-a63e-94dd5fc2a397)
// @Success 200 {array} Session
// @Failure 401 {object} KError "Missing jwt token"
// @Failure 403 {object} KError "Missing permissions: users.read."
// @Failure 404 {object} KError "No user found with id or username"
// @Router /users/{id}/sessions [get]
func (h *Handler) ListUserSessions(c *echo.Context) error {
ctx := c.Request().Context()
if err := CheckPermissions(c, []string{"users.read"}); err != nil {
return err
}
id := c.Param("id")
uid, err := uuid.Parse(id)
users, err := h.db.GetUser(ctx, dbc.GetUserParams{
UseId: err == nil,
Id: uid,
Username: id,
})
if err != nil {
return err
}
if len(users) == 0 {
return echo.NewHTTPError(http.StatusNotFound, "No user found with id or username")
}
dbSessions, err := h.db.GetUserSessions(ctx, users[0].User.Pk)
if err != nil {
return err
}
ret := make([]Session, 0, len(dbSessions))
for _, ses := range dbSessions {
ret = append(ret, MapSession(&ses))
}
return c.JSON(http.StatusOK, ret)
}
// @Summary Logout
// @Description Delete a session and logout
// @Tags sessions

61
auth/tests/sessions.hurl Normal file
View File

@ -0,0 +1,61 @@
# Setup first user
POST {{host}}/users
{
"username": "sessions-user-1",
"password": "password-sessions-user-1",
"email": "sessions-user-1@zoriya.dev"
}
HTTP 201
[Captures]
token1: jsonpath "$.token"
GET {{host}}/jwt
Authorization: Bearer {{token1}}
HTTP 200
[Captures]
jwt1: jsonpath "$.token"
GET {{host}}/users/me
Authorization: Bearer {{jwt1}}
HTTP 200
[Captures]
user1Id: jsonpath "$.id"
# Can list my own sessions
GET {{host}}/sessions
Authorization: Bearer {{jwt1}}
HTTP 200
[Captures]
session1Id: jsonpath "$[0].id"
# Setup second user
POST {{host}}/users
{
"username": "sessions-user-2",
"password": "password-sessions-user-2",
"email": "sessions-user-2@zoriya.dev"
}
HTTP 201
[Captures]
token2: jsonpath "$.token"
GET {{host}}/jwt
Authorization: Bearer {{token2}}
HTTP 200
[Captures]
jwt2: jsonpath "$.token"
# Cannot list another user's sessions without users.read
GET {{host}}/users/{{user1Id}}/sessions
Authorization: Bearer {{jwt2}}
HTTP 403
# Cleanup second user
DELETE {{host}}/users/me
Authorization: Bearer {{jwt2}}
HTTP 200
# Cleanup first user
DELETE {{host}}/users/me
Authorization: Bearer {{jwt1}}
HTTP 200

View File

@ -13,8 +13,8 @@ import (
)
func GetCurrentUserId(c *echo.Context) (uuid.UUID, error) {
user := c.Get("user").(*jwt.Token)
if user == nil {
user, ok := c.Get("user").(*jwt.Token)
if !ok || user == nil {
return uuid.UUID{}, echo.NewHTTPError(401, "Unauthorized")
}
sub, err := user.Claims.GetSubject()
@ -29,8 +29,8 @@ func GetCurrentUserId(c *echo.Context) (uuid.UUID, error) {
}
func GetCurrentSessionId(c *echo.Context) (uuid.UUID, error) {
user := c.Get("user").(*jwt.Token)
if user == nil {
user, ok := c.Get("user").(*jwt.Token)
if !ok || user == nil {
return uuid.UUID{}, echo.NewHTTPError(401, "Unauthorized")
}
claims, ok := user.Claims.(jwt.MapClaims)