Kyoo/auth/sessions.go
2025-04-05 23:49:53 +02:00

182 lines
5.2 KiB
Go

package main
import (
"cmp"
"context"
"crypto/rand"
"encoding/base64"
"net/http"
"time"
"github.com/alexedwards/argon2id"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/labstack/echo/v4"
"github.com/zoriya/kyoo/keibi/dbc"
)
type Session struct {
// Unique id of this session. Can be used for calls to DELETE
Id uuid.UUID `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"`
// When was the session first opened
CreatedDate time.Time `json:"createdDate" example:"2025-03-29T18:20:05.267Z"`
// Last date this session was used to access a service.
LastUsed time.Time `json:"lastUsed" example:"2025-03-29T18:20:05.267Z"`
// Device that created the session.
Device *string `json:"device" example:"Web - Firefox"`
}
type SessionWToken struct {
Session
Token string `json:"token" example:"lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="`
}
func MapSession(ses *dbc.Session) Session {
return Session{
Id: ses.Id,
CreatedDate: ses.CreatedDate,
LastUsed: ses.LastUsed,
Device: ses.Device,
}
}
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,
}
}
type LoginDto struct {
// Either the email or the username.
Login string `json:"login" validate:"required" example:"zoriya"`
// Password of the account.
Password string `json:"password" validate:"required" example:"password1234"`
}
// @Summary Login
// @Description Login to your account and open a session
// @Tags sessions
// @Accept json
// @Produce json
// @Param device query string false "The device the created session will be used on" example(android tv)
// @Param login body LoginDto false "Account informations"
// @Success 201 {object} SessionWToken
// @Failure 403 {object} KError "Invalid password"
// @Failure 404 {object} KError "Account does not exists"
// @Failure 422 {object} KError "User does not have a password (registered via oidc, please login via oidc)"
// @Router /sessions [post]
func (h *Handler) Login(c echo.Context) error {
var req LoginDto
err := c.Bind(&req)
if err != nil {
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
}
if err = c.Validate(&req); err != nil {
return err
}
dbuser, err := h.db.GetUserByLogin(context.Background(), req.Login)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "No account exists with the specified email or username.")
}
if dbuser.Password == nil {
return echo.NewHTTPError(http.StatusUnprocessableEntity, "Can't login with password, this account was created with OIDC.")
}
match, err := argon2id.ComparePasswordAndHash(req.Password, *dbuser.Password)
if err != nil {
return err
}
if !match {
return echo.NewHTTPError(http.StatusForbidden, "Invalid password")
}
user := MapDbUser(&dbuser)
return h.createSession(c, &user)
}
func (h *Handler) createSession(c echo.Context, user *User) error {
ctx := context.Background()
id := make([]byte, 64)
_, err := rand.Read(id)
if err != nil {
return err
}
dev := cmp.Or(c.Param("device"), c.Request().Header.Get("User-Agent"))
device := &dev
if dev == "" {
device = nil
}
session, err := h.db.CreateSession(ctx, dbc.CreateSessionParams{
Token: base64.RawURLEncoding.EncodeToString(id),
UserPk: user.Pk,
Device: device,
})
if err != nil {
return err
}
return c.JSON(201, MapSessionToken(&session))
}
// @Summary Logout
// @Description Delete a session and logout
// @Tags sessions
// @Produce json
// @Security Jwt
// @Success 200 {object} Session
// @Failure 401 {object} KError "Missing jwt token"
// @Failure 403 {object} KError "Invalid jwt token (or expired)"
// @Router /sessions/current [delete]
func (h *Handler) Logout(c echo.Context) error {
uid, err := GetCurrentUserId(c)
if err != nil {
return err
}
session := c.Param("id")
if session == "current" {
sid, ok := c.Get("user").(*jwt.Token).Claims.(jwt.MapClaims)["sid"]
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "Missing session id")
}
session = sid.(string)
}
sid, err := uuid.Parse(session)
if err != nil {
return echo.NewHTTPError(422, "Invalid session id")
}
ret, err := h.db.DeleteSession(context.Background(), dbc.DeleteSessionParams{
Id: sid,
UserId: uid,
})
if err == pgx.ErrNoRows {
return echo.NewHTTPError(404, "Session not found with specified id")
} else if err != nil {
return err
}
return c.JSON(200, MapSession(&ret))
}
// @Summary Delete other session
// @Description Delete a session and logout
// @Tags sessions
// @Produce json
// @Security Jwt
// @Param id path string true "The id of the session to delete" Format(uuid) Example(e05089d6-9179-4b5b-a63e-94dd5fc2a397)
// @Success 200 {object} Session
// @Failure 404 {object} KError "Session not found with specified id (if not using the /current route)"
// @Failure 422 {object} KError "Invalid session id"
// @Router /sessions/{id} [delete]
func DocOnly() {}