mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			219 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			219 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/rand"
 | |
| 	"encoding/base64"
 | |
| 	"fmt"
 | |
| 	"maps"
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"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 ApiKey struct {
 | |
| 	Id        uuid.UUID     `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"`
 | |
| 	Name      string        `json:"name" example:"myapp"`
 | |
| 	CreatedAt time.Time     `json:"createAt" example:"2025-03-29T18:20:05.267Z"`
 | |
| 	LastUsed  time.Time     `json:"lastUsed" example:"2025-03-29T18:20:05.267Z"`
 | |
| 	Claims    jwt.MapClaims `json:"claims" example:"isAdmin: true"`
 | |
| }
 | |
| 
 | |
| type ApiKeyWToken struct {
 | |
| 	ApiKey
 | |
| 	Token string `json:"token" example:"myapp-lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="`
 | |
| }
 | |
| 
 | |
| type ApiKeyDto struct {
 | |
| 	Name   string        `json:"name" example:"myapp" validate:"alpha"`
 | |
| 	Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"`
 | |
| }
 | |
| 
 | |
| func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
 | |
| 	return ApiKeyWToken{
 | |
| 		ApiKey: ApiKey{
 | |
| 			Id:        key.Id,
 | |
| 			Name:      key.Name,
 | |
| 			Claims:    key.Claims,
 | |
| 			CreatedAt: key.CreatedAt,
 | |
| 			LastUsed:  key.LastUsed,
 | |
| 		},
 | |
| 		Token: fmt.Sprintf("%s-%s", key.Name, key.Token),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // @Summary      Create API key
 | |
| // @Description  Create a new API key
 | |
| // @Tags         apikeys
 | |
| // @Accept       json
 | |
| // @Produce      json
 | |
| // @Security     Jwt[apikeys.write]
 | |
| // @Param        key  body      ApiKeyDto  false  "Api key info"
 | |
| // @Success      201  {object}  ApiKeyWToken
 | |
| // @Failure      409  {object}  KError "Duplicated api key"
 | |
| // @Failure      422  {object}  KError "Invalid create body"
 | |
| // @Router       /keys [post]
 | |
| func (h *Handler) CreateApiKey(c echo.Context) error {
 | |
| 	err := CheckPermissions(c, []string{"apikeys.write"})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	var req ApiKeyDto
 | |
| 	err = c.Bind(&req)
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
 | |
| 	}
 | |
| 	if err = c.Validate(&req); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if _, conflict := h.config.EnvApiKeys[req.Name]; conflict {
 | |
| 		return echo.NewHTTPError(409, "An env apikey is already defined with the same name")
 | |
| 	}
 | |
| 
 | |
| 	id := make([]byte, 64)
 | |
| 	_, err = rand.Read(id)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	var user *int32
 | |
| 	uid, err := GetCurrentUserId(c)
 | |
| 	// if err, we probably are using an api key (so no user)
 | |
| 	if err != nil {
 | |
| 		u, _ := h.db.GetUser(context.Background(), dbc.GetUserParams{
 | |
| 			UseId: true,
 | |
| 			Id:    uid,
 | |
| 		})
 | |
| 		user = &u[0].User.Pk
 | |
| 	}
 | |
| 
 | |
| 	dbkey, err := h.db.CreateApiKey(context.Background(), dbc.CreateApiKeyParams{
 | |
| 		Name:      req.Name,
 | |
| 		Token:     base64.RawURLEncoding.EncodeToString(id),
 | |
| 		Claims:    req.Claims,
 | |
| 		CreatedBy: user,
 | |
| 	})
 | |
| 	if ErrIs(err, pgerrcode.UniqueViolation) {
 | |
| 		return echo.NewHTTPError(409, "An apikey with the same name already exists.")
 | |
| 	} else if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return c.JSON(201, MapDbKey(&dbkey))
 | |
| }
 | |
| 
 | |
| // @Summary      Delete API key
 | |
| // @Description  Delete an existing API key
 | |
| // @Tags         apikeys
 | |
| // @Accept       json
 | |
| // @Produce      json
 | |
| // @Security     Jwt[apikeys.write]
 | |
| // @Success      200  {object}  ApiKey
 | |
| // @Failure      404  {object}  KError "Invalid id"
 | |
| // @Failure      422  {object}  KError "Invalid id format"
 | |
| // @Router       /keys [delete]
 | |
| func (h *Handler) DeleteApiKey(c echo.Context) error {
 | |
| 	err := CheckPermissions(c, []string{"apikeys.write"})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	id, err := uuid.Parse(c.Param("id"))
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(422, "Invalid id given: not an uuid")
 | |
| 	}
 | |
| 
 | |
| 	dbkey, err := h.db.DeleteApiKey(context.Background(), id)
 | |
| 	if err == pgx.ErrNoRows {
 | |
| 		return echo.NewHTTPError(404, "No apikey found")
 | |
| 	} else if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return c.JSON(200, MapDbKey(&dbkey).ApiKey)
 | |
| }
 | |
| 
 | |
| // @Summary      List API keys
 | |
| // @Description  List all api keys
 | |
| // @Tags         apikeys
 | |
| // @Accept       json
 | |
| // @Produce      json
 | |
| // @Security     Jwt[apikeys.read]
 | |
| // @Success      200  {object}  Page[ApiKey]
 | |
| // @Router       /keys [get]
 | |
| func (h *Handler) ListApiKey(c echo.Context) error {
 | |
| 	err := CheckPermissions(c, []string{"apikeys.read"})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	dbkeys, err := h.db.ListApiKeys(context.Background())
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	var ret []ApiKey
 | |
| 	for _, key := range dbkeys {
 | |
| 		ret = append(ret, MapDbKey(&key).ApiKey)
 | |
| 	}
 | |
| 
 | |
| 	for _, key := range h.config.EnvApiKeys {
 | |
| 		ret = append(ret, key.ApiKey)
 | |
| 	}
 | |
| 
 | |
| 	return c.JSON(200, Page[ApiKey]{
 | |
| 		Items: ret,
 | |
| 		This:  c.Request().URL.String(),
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (h *Handler) createApiJwt(apikey string) (string, error) {
 | |
| 	info := strings.SplitN(apikey, "-", 2)
 | |
| 	if len(info) != 2 {
 | |
| 		return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key format")
 | |
| 	}
 | |
| 
 | |
| 	key, fromEnv := h.config.EnvApiKeys[info[0]]
 | |
| 	if !fromEnv {
 | |
| 		dbKey, err := h.db.GetApiKey(context.Background(), dbc.GetApiKeyParams{
 | |
| 			Name:  info[0],
 | |
| 			Token: info[1],
 | |
| 		})
 | |
| 		if err == pgx.ErrNoRows {
 | |
| 			return "", echo.NewHTTPError(http.StatusForbidden, "Invalid api key")
 | |
| 		} else if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 
 | |
| 		go func() {
 | |
| 			h.db.TouchApiKey(context.Background(), dbKey.Pk)
 | |
| 		}()
 | |
| 
 | |
| 		key = MapDbKey(&dbKey)
 | |
| 	}
 | |
| 
 | |
| 	claims := maps.Clone(key.Claims)
 | |
| 	claims["username"] = key.Name
 | |
| 	claims["sub"] = key.Id
 | |
| 	claims["sid"] = key.Id
 | |
| 	claims["iss"] = h.config.PublicUrl
 | |
| 	claims["iat"] = &jwt.NumericDate{
 | |
| 		Time: time.Now().UTC(),
 | |
| 	}
 | |
| 	claims["exp"] = &jwt.NumericDate{
 | |
| 		Time: time.Now().UTC().Add(time.Hour),
 | |
| 	}
 | |
| 	jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
 | |
| 	t, err := jwt.SignedString(h.config.JwtPrivateKey)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return t, nil
 | |
| }
 |