mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
feat: sdk migration in progress
This commit is contained in:
175
internal/handlers/me_keys.go
Normal file
175
internal/handlers/me_keys.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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 Me / API Keys
|
||||
// @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 Me / API Keys
|
||||
// @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 Me / API Keys
|
||||
// @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
|
||||
}
|
||||
Reference in New Issue
Block a user