Add edit user/settings route

This commit is contained in:
Zoe Roux 2025-04-05 15:26:58 +02:00
parent a903d88a66
commit dbe8e319c8
No known key found for this signature in database
11 changed files with 365 additions and 16 deletions

View File

@ -93,3 +93,10 @@ RABBITMQ_HOST=rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_DEFAULT_USER=kyoo
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
# v5 stuff, does absolutely nothing on master (aka: you can delete this)
EXTRA_CLAIMS='{"permissions": [], "verified": false}'
FIRST_USER_CLAIMS='{"permissions": ["user.read", "users.write", "users.delete"], "verified": true}'
GUEST_CLAIMS='{"permissions": []}'
PROTECTED_CLAIMS="permissions,verified"

View File

@ -57,8 +57,3 @@ jobs:
working-directory: ./auth
run: cat logs
- uses: actions/upload-artifact@v4
with:
name: results
path: auth/out

View File

@ -14,6 +14,11 @@ EXTRA_CLAIMS='{}'
FIRST_USER_CLAIMS='{}'
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
GUEST_CLAIMS=""
# Comma separated list of claims that users without the `user.write` permissions should NOT be able to edit
# (if you don't specify this an user could make themself administrator for example)
# PS: `permissions` is always a protected claim since keibi uses it for user.read/user.write
PROTECTED_CLAIMS="permissions"
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
PUBLIC_URL=http://localhost:8901

View File

@ -8,6 +8,7 @@ import (
"encoding/pem"
"maps"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
@ -22,12 +23,14 @@ type Configuration struct {
DefaultClaims jwt.MapClaims
FirstUserClaims jwt.MapClaims
GuestClaims jwt.MapClaims
ProtectedClaims []string
ExpirationDelay time.Duration
}
var DefaultConfig = Configuration{
DefaultClaims: make(jwt.MapClaims),
FirstUserClaims: make(jwt.MapClaims),
ProtectedClaims: []string{"permissions"},
ExpirationDelay: 30 * 24 * time.Hour,
}
@ -64,6 +67,9 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
}
}
protected := strings.Split(os.Getenv("PROTECTED_CLAIMS"), ",")
ret.ProtectedClaims = append(ret.ProtectedClaims, protected...)
rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH")
if rsa_pk_path != "" {
privateKeyData, err := os.ReadFile(rsa_pk_path)

View File

@ -265,10 +265,10 @@ const updateUser = `-- name: UpdateUser :one
update
users
set
username = $2,
email = $3,
password = $4,
claims = $5
username = coalesce($2, username),
email = coalesce($3, email),
password = coalesce($4, password),
claims = coalesce($5, claims)
where
id = $1
returning
@ -277,8 +277,8 @@ returning
type UpdateUserParams struct {
Id uuid.UUID `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Username *string `json:"username"`
Email *string `json:"email"`
Password *string `json:"password"`
Claims jwt.MapClaims `json:"claims"`
}

View File

@ -377,6 +377,48 @@ const docTemplate = `{
}
}
}
},
"patch": {
"security": [
{
"Jwt": []
}
],
"description": "Edit your account's info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Edit self",
"parameters": [
{
"description": "Edited user info",
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
}
},
"403": {
"description": "You can't edit a protected claim",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/{id}": {
@ -469,10 +511,83 @@ const docTemplate = `{
}
}
}
},
"patch": {
"security": [
{
"Jwt": [
"users.write"
]
}
],
"description": "Edit an account info or permissions",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Edit user",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "User id of the user to delete",
"name": "id",
"in": "path"
},
{
"description": "Edited user info",
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
}
},
"403": {
"description": "You don't have permissions to edit another account",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
}
},
"definitions": {
"main.EditUserDto": {
"type": "object",
"properties": {
"claims": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"preferOriginal": " true"
}
},
"email": {
"type": "string",
"example": "kyoo@zoriya.dev"
},
"username": {
"type": "string",
"example": "zoriya"
}
}
},
"main.JwkSet": {
"type": "object",
"properties": {

View File

@ -371,6 +371,48 @@
}
}
}
},
"patch": {
"security": [
{
"Jwt": []
}
],
"description": "Edit your account's info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Edit self",
"parameters": [
{
"description": "Edited user info",
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
}
},
"403": {
"description": "You can't edit a protected claim",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
},
"/users/{id}": {
@ -463,10 +505,83 @@
}
}
}
},
"patch": {
"security": [
{
"Jwt": [
"users.write"
]
}
],
"description": "Edit an account info or permissions",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Edit user",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "User id of the user to delete",
"name": "id",
"in": "path"
},
{
"description": "Edited user info",
"name": "user",
"in": "body",
"schema": {
"$ref": "#/definitions/main.EditUserDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.User"
}
},
"403": {
"description": "You don't have permissions to edit another account",
"schema": {
"$ref": "#/definitions/main.KError"
}
}
}
}
}
},
"definitions": {
"main.EditUserDto": {
"type": "object",
"properties": {
"claims": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"preferOriginal": " true"
}
},
"email": {
"type": "string",
"example": "kyoo@zoriya.dev"
},
"username": {
"type": "string",
"example": "zoriya"
}
}
},
"main.JwkSet": {
"type": "object",
"properties": {

View File

@ -215,6 +215,8 @@ func main() {
r.GET("/users/me", h.GetMe)
r.DELETE("/users/:id", h.DeleteUser)
r.DELETE("/users/me", h.DeleteSelf)
r.PATCH("/users/:id", h.EditUser)
r.PATCH("/users/me", h.EditSelf)
g.POST("/users", h.Register)
g.POST("/sessions", h.Login)

View File

@ -125,7 +125,7 @@ func (h *Handler) createSession(c echo.Context, user *User) error {
if err != nil {
return err
}
return c.JSON(201, session)
return c.JSON(201, MapSessionToken(&session))
}
// @Summary Logout

View File

@ -67,10 +67,10 @@ returning
update
users
set
username = $2,
email = $3,
password = $4,
claims = $5
username = coalesce(sqlc.narg(username), username),
email = coalesce(sqlc.narg(email), email),
password = coalesce(sqlc.narg(password), password),
claims = coalesce(sqlc.narg(claims), claims)
where
id = $1
returning

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"fmt"
"net/http"
"time"
@ -51,6 +52,12 @@ type RegisterDto struct {
Password string `json:"password" validate:"required" example:"password1234"`
}
type EditUserDto struct {
Username *string `json:"username,omitempty" validate:"excludes=@" example:"zoriya"`
Email *string `json:"email,omitempty" validate:"email" example:"kyoo@zoriya.dev"`
Claims jwt.MapClaims `json:"claims,omitempty" example:"preferOriginal: true"`
}
func MapDbUser(user *dbc.User) User {
return User{
Pk: user.Pk,
@ -235,6 +242,11 @@ func (h *Handler) Register(c echo.Context) error {
// @Failure 404 {object} KError "Invalid user id"
// @Router /users/{id} [delete]
func (h *Handler) DeleteUser(c echo.Context) error {
err := CheckPermissions(c, []string{"user.delete"})
if err != nil {
return err
}
uid, err := uuid.Parse(c.Param("id"))
if err != nil {
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
@ -271,3 +283,95 @@ func (h *Handler) DeleteSelf(c echo.Context) error {
}
return c.JSON(200, MapDbUser(&ret))
}
// @Summary Edit self
// @Description Edit your account's info
// @Tags users
// @Accept json
// @Produce json
// @Security Jwt
// @Param user body EditUserDto false "Edited user info"
// @Success 200 {object} User
// @Success 403 {object} KError "You can't edit a protected claim"
// @Router /users/me [patch]
func (h *Handler) EditSelf(c echo.Context) error {
var req EditUserDto
err := c.Bind(&req)
if err != nil {
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
}
if err = c.Validate(&req); err != nil {
return err
}
for _, key := range h.config.ProtectedClaims {
if _, contains := req.Claims[key]; contains {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("Can't edit protected claim: '%s'.", key))
}
}
uid, err := GetCurrentUserId(c)
if err != nil {
return err
}
ret, err := h.db.UpdateUser(context.Background(), dbc.UpdateUserParams{
Id: uid,
Username: req.Username,
Email: req.Email,
Claims: req.Claims,
})
if err == pgx.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Invalid token, user not found.")
} else if err != nil {
return err
}
return c.JSON(200, MapDbUser(&ret))
}
// @Summary Edit user
// @Description Edit an account info or permissions
// @Tags users
// @Accept json
// @Produce json
// @Security Jwt[users.write]
// @Param id path string false "User id of the user to edit" Format(uuid)
// @Param user body EditUserDto false "Edited user info"
// @Success 200 {object} User
// @Success 403 {object} KError "You don't have permissions to edit another account"
// @Router /users/{id} [patch]
func (h *Handler) EditUser(c echo.Context) error {
err := CheckPermissions(c, []string{"user.write"})
if err != nil {
return err
}
uid, err := uuid.Parse(c.Param("id"))
if err != nil {
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
}
var req EditUserDto
err = c.Bind(&req)
if err != nil {
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
}
if err = c.Validate(&req); err != nil {
return err
}
ret, err := h.db.UpdateUser(context.Background(), dbc.UpdateUserParams{
Id: uid,
Username: req.Username,
Email: req.Email,
Claims: req.Claims,
})
if err == pgx.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Invalid user id, user not found")
} else if err != nil {
return err
}
return c.JSON(200, MapDbUser(&ret))
}