mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add apikeys routes
This commit is contained in:
parent
822a7029ef
commit
85186a74c8
@ -1,26 +1,50 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"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/labstack/echo/v4"
|
||||||
|
"github.com/zoriya/kyoo/keibi/dbc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
Name string `json:"name" example:"my-app"`
|
Name string `json:"name" example:"myapp"`
|
||||||
Token string `json:"token" example:"lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="`
|
|
||||||
CreatedAt time.Time `json:"createAt" example:"2025-03-29T18:20:05.267Z"`
|
CreatedAt time.Time `json:"createAt" example:"2025-03-29T18:20:05.267Z"`
|
||||||
LastUsed time.Time `json:"lastUsed" 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"`
|
Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiKeyWToken struct {
|
||||||
|
ApiKey
|
||||||
|
Token string `json:"token" example:"myapp-lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="`
|
||||||
|
}
|
||||||
|
|
||||||
type ApiKeyDto struct {
|
type ApiKeyDto struct {
|
||||||
Name string `json:"name" example:"my-app" validate:"alpha"`
|
Name string `json:"name" example:"my-app" validate:"alpha"`
|
||||||
Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"`
|
Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MapDbKey(key *dbc.Apikey) ApiKeyWToken {
|
||||||
|
return ApiKeyWToken{
|
||||||
|
ApiKey: ApiKey{
|
||||||
|
Name: key.Name,
|
||||||
|
Claims: key.Claims,
|
||||||
|
CreatedAt: key.CreatedAt,
|
||||||
|
LastUsed: key.LastUsed,
|
||||||
|
},
|
||||||
|
Token: key.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Create API key
|
// @Summary Create API key
|
||||||
// @Description Create a new API key
|
// @Description Create a new API key
|
||||||
// @Tags apikeys
|
// @Tags apikeys
|
||||||
@ -28,10 +52,10 @@ type ApiKeyDto struct {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security Jwt[apikeys.write]
|
// @Security Jwt[apikeys.write]
|
||||||
// @Param key body ApiKeyDto false "Api key info"
|
// @Param key body ApiKeyDto false "Api key info"
|
||||||
// @Success 201 {object} ApiKey
|
// @Success 201 {object} ApiKeyWToken
|
||||||
// @Failure 409 {object} KError "Duplicated api key"
|
// @Failure 409 {object} KError "Duplicated api key"
|
||||||
// @Failure 422 {object} KError "Invalid create body"
|
// @Failure 422 {object} KError "Invalid create body"
|
||||||
// @Router /users [get]
|
// @Router /keys [post]
|
||||||
func (h *Handler) CreateApiKey(c echo.Context) error {
|
func (h *Handler) CreateApiKey(c echo.Context) error {
|
||||||
var req ApiKeyDto
|
var req ApiKeyDto
|
||||||
err := c.Bind(&req)
|
err := c.Bind(&req)
|
||||||
@ -41,4 +65,70 @@ func (h *Handler) CreateApiKey(c echo.Context) error {
|
|||||||
if err = c.Validate(&req); err != nil {
|
if err = c.Validate(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: fmt.Sprintf("%s-%s", req.Name, 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)
|
||||||
|
}
|
||||||
|
return c.JSON(200, Page[ApiKey]{
|
||||||
|
Items: ret,
|
||||||
|
This: c.Request().URL.String(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
103
auth/dbc/apikeys.sql.go
Normal file
103
auth/dbc/apikeys.sql.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: apikeys.sql
|
||||||
|
|
||||||
|
package dbc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
jwt "github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createApiKey = `-- name: CreateApiKey :one
|
||||||
|
insert into apikeys(name, token, claims)
|
||||||
|
values ($1, $2, $3)
|
||||||
|
returning
|
||||||
|
pk, id, name, token, claims, created_by, created_at, last_used
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateApiKeyParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Claims jwt.MapClaims `json:"claims"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateApiKey(ctx context.Context, arg CreateApiKeyParams) (Apikey, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createApiKey, arg.Name, arg.Token, arg.Claims)
|
||||||
|
var i Apikey
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Pk,
|
||||||
|
&i.Id,
|
||||||
|
&i.Name,
|
||||||
|
&i.Token,
|
||||||
|
&i.Claims,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.LastUsed,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteApiKey = `-- name: DeleteApiKey :one
|
||||||
|
delete from apikeys
|
||||||
|
where id = $1
|
||||||
|
returning
|
||||||
|
pk, id, name, token, claims, created_by, created_at, last_used
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteApiKey(ctx context.Context, id uuid.UUID) (Apikey, error) {
|
||||||
|
row := q.db.QueryRow(ctx, deleteApiKey, id)
|
||||||
|
var i Apikey
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Pk,
|
||||||
|
&i.Id,
|
||||||
|
&i.Name,
|
||||||
|
&i.Token,
|
||||||
|
&i.Claims,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.LastUsed,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listApiKeys = `-- name: ListApiKeys :many
|
||||||
|
select
|
||||||
|
pk, id, name, token, claims, created_by, created_at, last_used
|
||||||
|
from
|
||||||
|
apikeys
|
||||||
|
order by
|
||||||
|
last_used
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListApiKeys(ctx context.Context) ([]Apikey, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listApiKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Apikey
|
||||||
|
for rows.Next() {
|
||||||
|
var i Apikey
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Pk,
|
||||||
|
&i.Id,
|
||||||
|
&i.Name,
|
||||||
|
&i.Token,
|
||||||
|
&i.Claims,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.LastUsed,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
@ -11,6 +11,17 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Apikey struct {
|
||||||
|
Pk int32 `json:"pk"`
|
||||||
|
Id uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Claims jwt.MapClaims `json:"claims"`
|
||||||
|
CreatedBy int32 `json:"createdBy"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
LastUsed time.Time `json:"lastUsed"`
|
||||||
|
}
|
||||||
|
|
||||||
type OidcHandle struct {
|
type OidcHandle struct {
|
||||||
UserPk int32 `json:"userPk"`
|
UserPk int32 `json:"userPk"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
@ -224,6 +224,10 @@ func main() {
|
|||||||
r.DELETE("/sessions", h.Logout)
|
r.DELETE("/sessions", h.Logout)
|
||||||
r.DELETE("/sessions/:id", h.Logout)
|
r.DELETE("/sessions/:id", h.Logout)
|
||||||
|
|
||||||
|
r.GET("/keys", h.ListApiKey)
|
||||||
|
r.POST("/keys", h.CreateApiKey)
|
||||||
|
r.DELETE("/keys", h.DeleteApiKey)
|
||||||
|
|
||||||
g.GET("/jwt", h.CreateJwt)
|
g.GET("/jwt", h.CreateJwt)
|
||||||
e.GET("/.well-known/jwks.json", h.GetJwks)
|
e.GET("/.well-known/jwks.json", h.GetJwks)
|
||||||
e.GET("/.well-known/openid-configuration", h.GetOidcConfig)
|
e.GET("/.well-known/openid-configuration", h.GetOidcConfig)
|
||||||
|
@ -7,6 +7,7 @@ create table apikeys(
|
|||||||
token varchar(128) not null unique,
|
token varchar(128) not null unique,
|
||||||
claims jsonb not null,
|
claims jsonb not null,
|
||||||
|
|
||||||
|
created_by integer not null references users(pk) on delete cascade,
|
||||||
created_at timestamptz not null default now()::timestamptz,
|
created_at timestamptz not null default now()::timestamptz,
|
||||||
last_used timestamptz not null default now()::temistamptz
|
last_used timestamptz not null default now()::temistamptz
|
||||||
);
|
);
|
||||||
|
20
auth/sql/queries/apikeys.sql
Normal file
20
auth/sql/queries/apikeys.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- name: ListApiKeys :many
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
apikeys
|
||||||
|
order by
|
||||||
|
last_used;
|
||||||
|
|
||||||
|
-- name: CreateApiKey :one
|
||||||
|
insert into apikeys(name, token, claims)
|
||||||
|
values ($1, $2, $3)
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteApiKey :one
|
||||||
|
delete from apikeys
|
||||||
|
where id = $1
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
@ -35,5 +35,10 @@ sql:
|
|||||||
import: "github.com/golang-jwt/jwt/v5"
|
import: "github.com/golang-jwt/jwt/v5"
|
||||||
package: "jwt"
|
package: "jwt"
|
||||||
type: "MapClaims"
|
type: "MapClaims"
|
||||||
|
- column: "apikeys.claims"
|
||||||
|
go_type:
|
||||||
|
import: "github.com/golang-jwt/jwt/v5"
|
||||||
|
package: "jwt"
|
||||||
|
type: "MapClaims"
|
||||||
|
|
||||||
|
|
||||||
|
@ -250,8 +250,8 @@ func (h *Handler) Register(c echo.Context) error {
|
|||||||
// @Security Jwt[users.delete]
|
// @Security Jwt[users.delete]
|
||||||
// @Param id path string false "User id of the user to delete" Format(uuid)
|
// @Param id path string false "User id of the user to delete" Format(uuid)
|
||||||
// @Success 200 {object} User
|
// @Success 200 {object} User
|
||||||
// @Failure 404 {object} KError "Invalid id format"
|
|
||||||
// @Failure 404 {object} KError "Invalid user id"
|
// @Failure 404 {object} KError "Invalid user id"
|
||||||
|
// @Failure 422 {object} KError "Invalid id format"
|
||||||
// @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{"users.delete"})
|
err := CheckPermissions(c, []string{"users.delete"})
|
||||||
@ -261,7 +261,7 @@ func (h *Handler) DeleteUser(c echo.Context) error {
|
|||||||
|
|
||||||
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(422, "Invalid id given: not an uuid")
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := h.db.DeleteUser(context.Background(), uid)
|
ret, err := h.db.DeleteUser(context.Background(), uid)
|
||||||
|
@ -11,7 +11,6 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def get_ignore_pattern():
|
def get_ignore_pattern():
|
||||||
"""Compile ignore pattern from environment variable."""
|
|
||||||
try:
|
try:
|
||||||
pattern = os.environ.get("LIBRARY_IGNORE_PATTERN")
|
pattern = os.environ.get("LIBRARY_IGNORE_PATTERN")
|
||||||
return re.compile(pattern) if pattern else None
|
return re.compile(pattern) if pattern else None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user