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 }