Kyoo/auth/apikey.go
2025-04-23 19:41:42 +02:00

192 lines
4.9 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:"my-app" 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 {
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
}
dbkey, err := h.db.CreateApiKey(context.Background(), dbc.CreateApiKeyParams{
Name: req.Name,
Token: base64.RawURLEncoding.EncodeToString(id),
Claims: req.Claims,
})
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 {
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 {
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.Split(apikey, "-")
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
}