mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
179 lines
4.9 KiB
Go
179 lines
4.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
|
"github.com/glueops/autoglue/internal/auth"
|
|
"github.com/glueops/autoglue/internal/models"
|
|
"github.com/glueops/autoglue/internal/utils"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type userAPIKeyOut struct {
|
|
ID uuid.UUID `json:"id" format:"uuid"`
|
|
Name *string `json:"name,omitempty"`
|
|
Scope string `json:"scope"` // "user"
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
|
Plain *string `json:"plain,omitempty"` // Shown only on create:
|
|
}
|
|
|
|
// ListUserAPIKeys godoc
|
|
//
|
|
// @ID ListUserAPIKeys
|
|
// @Summary List my API keys
|
|
// @Tags MeAPIKeys
|
|
// @Produce json
|
|
// @Success 200 {array} userAPIKeyOut
|
|
// @Router /me/api-keys [get]
|
|
// @Security BearerAuth
|
|
// @Security ApiKeyAuth
|
|
func ListUserAPIKeys(db *gorm.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
|
if !ok {
|
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
|
return
|
|
}
|
|
var rows []models.APIKey
|
|
if err := db.
|
|
Where("scope = ? AND user_id = ?", "user", u.ID).
|
|
Order("created_at desc").
|
|
Find(&rows).Error; err != nil {
|
|
utils.WriteError(w, 500, "db_error", err.Error())
|
|
return
|
|
}
|
|
out := make([]userAPIKeyOut, 0, len(rows))
|
|
for _, k := range rows {
|
|
out = append(out, toUserKeyOut(k, nil))
|
|
}
|
|
utils.WriteJSON(w, 200, out)
|
|
}
|
|
}
|
|
|
|
type createUserKeyRequest struct {
|
|
Name string `json:"name,omitempty"`
|
|
ExpiresInHours *int `json:"expires_in_hours,omitempty"` // optional TTL
|
|
}
|
|
|
|
// CreateUserAPIKey godoc
|
|
//
|
|
// @ID CreateUserAPIKey
|
|
// @Summary Create a new user API key
|
|
// @Description Returns the plaintext key once. Store it securely on the client side.
|
|
// @Tags MeAPIKeys
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body createUserKeyRequest true "Key options"
|
|
// @Success 201 {object} userAPIKeyOut
|
|
// @Router /me/api-keys [post]
|
|
// @Security BearerAuth
|
|
// @Security ApiKeyAuth
|
|
func CreateUserAPIKey(db *gorm.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
|
if !ok {
|
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
|
return
|
|
}
|
|
var req createUserKeyRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
utils.WriteError(w, 400, "invalid_json", err.Error())
|
|
return
|
|
}
|
|
|
|
plain, err := generateUserAPIKey()
|
|
if err != nil {
|
|
utils.WriteError(w, 500, "gen_failed", err.Error())
|
|
return
|
|
}
|
|
hash := auth.SHA256Hex(plain)
|
|
|
|
var exp *time.Time
|
|
if req.ExpiresInHours != nil && *req.ExpiresInHours > 0 {
|
|
t := time.Now().Add(time.Duration(*req.ExpiresInHours) * time.Hour)
|
|
exp = &t
|
|
}
|
|
|
|
rec := models.APIKey{
|
|
Scope: "user",
|
|
UserID: &u.ID,
|
|
KeyHash: hash,
|
|
Name: req.Name, // if field exists
|
|
ExpiresAt: exp,
|
|
// SecretHash: nil (not used for user keys)
|
|
}
|
|
if err := db.Create(&rec).Error; err != nil {
|
|
utils.WriteError(w, 500, "db_error", err.Error())
|
|
return
|
|
}
|
|
utils.WriteJSON(w, http.StatusCreated, toUserKeyOut(rec, &plain))
|
|
}
|
|
}
|
|
|
|
// DeleteUserAPIKey godoc
|
|
//
|
|
// @ID DeleteUserAPIKey
|
|
// @Summary Delete a user API key
|
|
// @Tags MeAPIKeys
|
|
// @Produce json
|
|
// @Param id path string true "Key ID (UUID)"
|
|
// @Success 204 "No Content"
|
|
// @Router /me/api-keys/{id} [delete]
|
|
// @Security BearerAuth
|
|
func DeleteUserAPIKey(db *gorm.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
u, ok := httpmiddleware.UserFrom(r.Context())
|
|
if !ok {
|
|
utils.WriteError(w, http.StatusUnauthorized, "unauthorized", "not signed in")
|
|
return
|
|
}
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
utils.WriteError(w, 400, "invalid_id", "must be uuid")
|
|
return
|
|
}
|
|
tx := db.Where("id = ? AND scope = ? AND user_id = ?", id, "user", u.ID).
|
|
Delete(&models.APIKey{})
|
|
if tx.Error != nil {
|
|
utils.WriteError(w, 500, "db_error", tx.Error.Error())
|
|
return
|
|
}
|
|
if tx.RowsAffected == 0 {
|
|
utils.WriteError(w, 404, "not_found", "key not found")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func toUserKeyOut(k models.APIKey, plain *string) userAPIKeyOut {
|
|
return userAPIKeyOut{
|
|
ID: k.ID,
|
|
Name: &k.Name, // if your model has it; else remove
|
|
Scope: k.Scope,
|
|
CreatedAt: k.CreatedAt,
|
|
ExpiresAt: k.ExpiresAt,
|
|
LastUsedAt: k.LastUsedAt, // if present; else remove
|
|
Plain: plain,
|
|
}
|
|
}
|
|
|
|
func generateUserAPIKey() (string, error) {
|
|
// 24 random bytes → base64url (no padding), with "u_" prefix
|
|
b := make([]byte, 24)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
s := base64.RawURLEncoding.EncodeToString(b)
|
|
return "u_" + s, nil
|
|
}
|