mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			182 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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() {}
 |