mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add edit user/settings route
This commit is contained in:
parent
a903d88a66
commit
dbe8e319c8
@ -93,3 +93,10 @@ RABBITMQ_HOST=rabbitmq
|
|||||||
RABBITMQ_PORT=5672
|
RABBITMQ_PORT=5672
|
||||||
RABBITMQ_DEFAULT_USER=kyoo
|
RABBITMQ_DEFAULT_USER=kyoo
|
||||||
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
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"
|
||||||
|
5
.github/workflows/auth-hurl.yml
vendored
5
.github/workflows/auth-hurl.yml
vendored
@ -57,8 +57,3 @@ jobs:
|
|||||||
working-directory: ./auth
|
working-directory: ./auth
|
||||||
run: cat logs
|
run: cat logs
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: results
|
|
||||||
path: auth/out
|
|
||||||
|
|
||||||
|
@ -14,6 +14,11 @@ EXTRA_CLAIMS='{}'
|
|||||||
FIRST_USER_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`)
|
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
|
||||||
GUEST_CLAIMS=""
|
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.
|
# 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
|
PUBLIC_URL=http://localhost:8901
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@ -22,12 +23,14 @@ type Configuration struct {
|
|||||||
DefaultClaims jwt.MapClaims
|
DefaultClaims jwt.MapClaims
|
||||||
FirstUserClaims jwt.MapClaims
|
FirstUserClaims jwt.MapClaims
|
||||||
GuestClaims jwt.MapClaims
|
GuestClaims jwt.MapClaims
|
||||||
|
ProtectedClaims []string
|
||||||
ExpirationDelay time.Duration
|
ExpirationDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultConfig = Configuration{
|
var DefaultConfig = Configuration{
|
||||||
DefaultClaims: make(jwt.MapClaims),
|
DefaultClaims: make(jwt.MapClaims),
|
||||||
FirstUserClaims: make(jwt.MapClaims),
|
FirstUserClaims: make(jwt.MapClaims),
|
||||||
|
ProtectedClaims: []string{"permissions"},
|
||||||
ExpirationDelay: 30 * 24 * time.Hour,
|
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")
|
rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH")
|
||||||
if rsa_pk_path != "" {
|
if rsa_pk_path != "" {
|
||||||
privateKeyData, err := os.ReadFile(rsa_pk_path)
|
privateKeyData, err := os.ReadFile(rsa_pk_path)
|
||||||
|
@ -265,10 +265,10 @@ const updateUser = `-- name: UpdateUser :one
|
|||||||
update
|
update
|
||||||
users
|
users
|
||||||
set
|
set
|
||||||
username = $2,
|
username = coalesce($2, username),
|
||||||
email = $3,
|
email = coalesce($3, email),
|
||||||
password = $4,
|
password = coalesce($4, password),
|
||||||
claims = $5
|
claims = coalesce($5, claims)
|
||||||
where
|
where
|
||||||
id = $1
|
id = $1
|
||||||
returning
|
returning
|
||||||
@ -277,8 +277,8 @@ returning
|
|||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateUserParams struct {
|
||||||
Id uuid.UUID `json:"id"`
|
Id uuid.UUID `json:"id"`
|
||||||
Username string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email *string `json:"email"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
Claims jwt.MapClaims `json:"claims"`
|
Claims jwt.MapClaims `json:"claims"`
|
||||||
}
|
}
|
||||||
|
@ -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}": {
|
"/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": {
|
"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": {
|
"main.JwkSet": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -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}": {
|
"/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": {
|
"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": {
|
"main.JwkSet": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -215,6 +215,8 @@ func main() {
|
|||||||
r.GET("/users/me", h.GetMe)
|
r.GET("/users/me", h.GetMe)
|
||||||
r.DELETE("/users/:id", h.DeleteUser)
|
r.DELETE("/users/:id", h.DeleteUser)
|
||||||
r.DELETE("/users/me", h.DeleteSelf)
|
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("/users", h.Register)
|
||||||
|
|
||||||
g.POST("/sessions", h.Login)
|
g.POST("/sessions", h.Login)
|
||||||
|
@ -125,7 +125,7 @@ func (h *Handler) createSession(c echo.Context, user *User) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.JSON(201, session)
|
return c.JSON(201, MapSessionToken(&session))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Logout
|
// @Summary Logout
|
||||||
|
@ -67,10 +67,10 @@ returning
|
|||||||
update
|
update
|
||||||
users
|
users
|
||||||
set
|
set
|
||||||
username = $2,
|
username = coalesce(sqlc.narg(username), username),
|
||||||
email = $3,
|
email = coalesce(sqlc.narg(email), email),
|
||||||
password = $4,
|
password = coalesce(sqlc.narg(password), password),
|
||||||
claims = $5
|
claims = coalesce(sqlc.narg(claims), claims)
|
||||||
where
|
where
|
||||||
id = $1
|
id = $1
|
||||||
returning
|
returning
|
||||||
|
104
auth/users.go
104
auth/users.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -51,6 +52,12 @@ type RegisterDto struct {
|
|||||||
Password string `json:"password" validate:"required" example:"password1234"`
|
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 {
|
func MapDbUser(user *dbc.User) User {
|
||||||
return User{
|
return User{
|
||||||
Pk: user.Pk,
|
Pk: user.Pk,
|
||||||
@ -235,6 +242,11 @@ func (h *Handler) Register(c echo.Context) error {
|
|||||||
// @Failure 404 {object} KError "Invalid user id"
|
// @Failure 404 {object} KError "Invalid user id"
|
||||||
// @Router /users/{id} [delete]
|
// @Router /users/{id} [delete]
|
||||||
func (h *Handler) DeleteUser(c echo.Context) error {
|
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"))
|
uid, err := uuid.Parse(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
|
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))
|
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))
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user