mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			273 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"net/http"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/alexedwards/argon2id"
 | 
						|
	"github.com/golang-jwt/jwt/v5"
 | 
						|
	"github.com/google/uuid"
 | 
						|
	"github.com/jackc/pgerrcode"
 | 
						|
	"github.com/jackc/pgx/v5"
 | 
						|
	"github.com/labstack/echo/v4"
 | 
						|
	"github.com/zoriya/kyoo/keibi/dbc"
 | 
						|
)
 | 
						|
 | 
						|
type User struct {
 | 
						|
	// Primary key in database
 | 
						|
	Pk int32 `json:"-"`
 | 
						|
	// Id of the user.
 | 
						|
	Id uuid.UUID `json:"id"`
 | 
						|
	// Username of the user. Can be used as a login.
 | 
						|
	Username string `json:"username"`
 | 
						|
	// Email of the user. Can be used as a login.
 | 
						|
	Email string `json:"email" format:"email"`
 | 
						|
	// When was this account created?
 | 
						|
	CreatedDate time.Time `json:"createdDate"`
 | 
						|
	// When was the last time this account made any authorized request?
 | 
						|
	LastSeen time.Time `json:"lastSeen"`
 | 
						|
	// List of custom claims JWT created via get /jwt will have
 | 
						|
	Claims jwt.MapClaims `json:"claims"`
 | 
						|
	// List of other login method available for this user. Access tokens wont be returned here.
 | 
						|
	Oidc map[string]OidcHandle `json:"oidc,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
type OidcHandle struct {
 | 
						|
	// Id of this oidc handle.
 | 
						|
	Id string `json:"id"`
 | 
						|
	// Username of the user on the external service.
 | 
						|
	Username string `json:"username"`
 | 
						|
	// Link to the profile of the user on the external service. Null if unknown or irrelevant.
 | 
						|
	ProfileUrl *string `json:"profileUrl" format:"url"`
 | 
						|
}
 | 
						|
 | 
						|
type RegisterDto struct {
 | 
						|
	// Username of the new account, can't contain @ signs. Can be used for login.
 | 
						|
	Username string `json:"username" validate:"required,excludes=@"`
 | 
						|
	// Valid email that could be used for forgotten password requests. Can be used for login.
 | 
						|
	Email string `json:"email" validate:"required,email" format:"email"`
 | 
						|
	// Password to use.
 | 
						|
	Password string `json:"password" validate:"required"`
 | 
						|
}
 | 
						|
 | 
						|
func MapDbUser(user *dbc.User) User {
 | 
						|
	return User{
 | 
						|
		Pk:          user.Pk,
 | 
						|
		Id:          user.Id,
 | 
						|
		Username:    user.Username,
 | 
						|
		Email:       user.Email,
 | 
						|
		CreatedDate: user.CreatedDate,
 | 
						|
		LastSeen:    user.LastSeen,
 | 
						|
		Claims:      user.Claims,
 | 
						|
		Oidc:        nil,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func MapOidc(oidc *dbc.GetUserRow) OidcHandle {
 | 
						|
	return OidcHandle{
 | 
						|
		Id:         *oidc.Id,
 | 
						|
		Username:   *oidc.Username,
 | 
						|
		ProfileUrl: oidc.ProfileUrl,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// @Summary      List all users
 | 
						|
// @Description  List all users existing in this instance.
 | 
						|
// @Tags         users
 | 
						|
// @Accept       json
 | 
						|
// @Produce      json
 | 
						|
// @Security     Jwt[users.read]
 | 
						|
// @Param        afterId   query      string  false  "used for pagination." Format(uuid)
 | 
						|
// @Success      200  {object}  User[]
 | 
						|
// @Failure      400  {object}  problem.Problem "Invalid after id"
 | 
						|
// @Router       /users [get]
 | 
						|
func (h *Handler) ListUsers(c echo.Context) error {
 | 
						|
	err := CheckPermissions(c, []string{"user.read"})
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	ctx := context.Background()
 | 
						|
	limit := int32(20)
 | 
						|
	id := c.Param("afterId")
 | 
						|
 | 
						|
	var users []dbc.User
 | 
						|
	if id == "" {
 | 
						|
		users, err = h.db.GetAllUsers(ctx, limit)
 | 
						|
	} else {
 | 
						|
		uid, uerr := uuid.Parse(id)
 | 
						|
		if uerr != nil {
 | 
						|
			return echo.NewHTTPError(400, "Invalid `afterId` parameter, uuid was expected")
 | 
						|
		}
 | 
						|
		users, err = h.db.GetAllUsersAfter(ctx, dbc.GetAllUsersAfterParams{
 | 
						|
			Limit:   limit,
 | 
						|
			AfterId: uid,
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	var ret []User
 | 
						|
	for _, user := range users {
 | 
						|
		ret = append(ret, MapDbUser(&user))
 | 
						|
	}
 | 
						|
	// TODO: switch to a Page
 | 
						|
	return c.JSON(200, ret)
 | 
						|
}
 | 
						|
 | 
						|
// @Summary      Get user
 | 
						|
// @Description  Get informations about a user from it's id
 | 
						|
// @Tags         users
 | 
						|
// @Produce      json
 | 
						|
// @Security     Jwt[users.read]
 | 
						|
// @Param        id   path      string    true  "The id of the user" Format(uuid)
 | 
						|
// @Success      200  {object}  User
 | 
						|
// @Failure      404  {object}  problem.Problem "No user with the given id found"
 | 
						|
// @Router /users/{id} [get]
 | 
						|
func (h *Handler) GetUser(c echo.Context) error {
 | 
						|
	err := CheckPermissions(c, []string{"user.read"})
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	id, err := uuid.Parse(c.Param("id"))
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(400, "Invalid id")
 | 
						|
	}
 | 
						|
	dbuser, err := h.db.GetUser(context.Background(), id)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	user := MapDbUser(&dbuser[0].User)
 | 
						|
	for _, oidc := range dbuser {
 | 
						|
		if oidc.Provider != nil {
 | 
						|
			user.Oidc[*oidc.Provider] = MapOidc(&oidc)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return c.JSON(200, user)
 | 
						|
}
 | 
						|
 | 
						|
// @Summary      Get me
 | 
						|
// @Description  Get informations about the currently connected user
 | 
						|
// @Tags         users
 | 
						|
// @Produce      json
 | 
						|
// @Security     Jwt
 | 
						|
// @Success      200  {object}  User
 | 
						|
// @Failure      401  {object}  problem.Problem "Missing jwt token"
 | 
						|
// @Failure      403  {object}  problem.Problem "Invalid jwt token (or expired)"
 | 
						|
// @Router /users/me [get]
 | 
						|
func (h *Handler) GetMe(c echo.Context) error {
 | 
						|
	id, err := GetCurrentUserId(c)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	dbuser, err := h.db.GetUser(context.Background(), id)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	user := MapDbUser(&dbuser[0].User)
 | 
						|
	for _, oidc := range dbuser {
 | 
						|
		if oidc.Provider != nil {
 | 
						|
			user.Oidc[*oidc.Provider] = MapOidc(&oidc)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return c.JSON(200, user)
 | 
						|
}
 | 
						|
 | 
						|
// @Summary      Register
 | 
						|
// @Description  Register as a new user and open a session for it
 | 
						|
// @Tags         users
 | 
						|
// @Accept       json
 | 
						|
// @Produce      json
 | 
						|
// @Param        device   query   string         false  "The device the created session will be used on"
 | 
						|
// @Param        user     body    RegisterDto  false  "Registration informations"
 | 
						|
// @Success      201  {object}  dbc.Session
 | 
						|
// @Failure      400  {object}  problem.Problem "Invalid register body"
 | 
						|
// @Success      409  {object}  problem.Problem "Duplicated email or username"
 | 
						|
// @Router /users [post]
 | 
						|
func (h *Handler) Register(c echo.Context) error {
 | 
						|
	var req RegisterDto
 | 
						|
	err := c.Bind(&req)
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
 | 
						|
	}
 | 
						|
	if err = c.Validate(&req); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	pass, err := argon2id.CreateHash(req.Password, argon2id.DefaultParams)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	duser, err := h.db.CreateUser(context.Background(), dbc.CreateUserParams{
 | 
						|
		Username: req.Username,
 | 
						|
		Email:    req.Email,
 | 
						|
		Password: &pass,
 | 
						|
		Claims:   h.config.DefaultClaims,
 | 
						|
	})
 | 
						|
	if ErrIs(err, pgerrcode.UniqueViolation) {
 | 
						|
		return echo.NewHTTPError(409, "Email or username already taken")
 | 
						|
	} else if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	user := MapDbUser(&duser)
 | 
						|
	return h.createSession(c, &user)
 | 
						|
}
 | 
						|
 | 
						|
// @Summary      Delete user
 | 
						|
// @Description  Delete an account and all it's sessions.
 | 
						|
// @Tags         users
 | 
						|
// @Accept       json
 | 
						|
// @Produce      json
 | 
						|
// @Security     Jwt[users.delete]
 | 
						|
// @Param        id   path      string  false  "User id of the user to delete" Format(uuid)
 | 
						|
// @Success      200  {object}  User
 | 
						|
// @Failure      404  {object}  problem.Problem "Invalid id format"
 | 
						|
// @Failure      404  {object}  problem.Problem "Invalid user id"
 | 
						|
// @Router /users/{id} [delete]
 | 
						|
func (h *Handler) DeleteUser(c echo.Context) error {
 | 
						|
	uid, err := uuid.Parse(c.Param("id"))
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(400, "Invalid id given: not an uuid")
 | 
						|
	}
 | 
						|
 | 
						|
	ret, err := h.db.DeleteUser(context.Background(), uid)
 | 
						|
	if err == pgx.ErrNoRows {
 | 
						|
		return echo.NewHTTPError(404, "No user found with given id")
 | 
						|
	} else if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return c.JSON(200, MapDbUser(&ret))
 | 
						|
}
 | 
						|
 | 
						|
// @Summary      Delete self
 | 
						|
// @Description  Delete your account and all your sessions
 | 
						|
// @Tags         users
 | 
						|
// @Accept       json
 | 
						|
// @Produce      json
 | 
						|
// @Security     Jwt
 | 
						|
// @Success      200  {object}  User
 | 
						|
// @Router /users/me [delete]
 | 
						|
func (h *Handler) DeleteSelf(c echo.Context) error {
 | 
						|
	uid, err := GetCurrentUserId(c)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	ret, err := h.db.DeleteUser(context.Background(), uid)
 | 
						|
	if err == pgx.ErrNoRows {
 | 
						|
		return echo.NewHTTPError(403, "Invalid token, user already deleted.")
 | 
						|
	} else if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return c.JSON(200, MapDbUser(&ret))
 | 
						|
}
 |