feat: refactor api key service

This commit is contained in:
monomarh
2025-11-02 18:26:46 +01:00
parent b743493bb7
commit 53d438e8ed
12 changed files with 66 additions and 106 deletions

View File

@@ -34,7 +34,7 @@ type AuthenticateMiddleware struct {
optionalForMethods []string
redirectTarget string // optional
redirectErrorMessage string // optional
onlyRWApiKeys bool
onlyRWApiKey bool
}
func NewAuthenticateMiddleware(userService services.IUserService) *AuthenticateMiddleware {
@@ -43,7 +43,7 @@ func NewAuthenticateMiddleware(userService services.IUserService) *AuthenticateM
userSrvc: userService,
optionalForPaths: []string{},
optionalForMethods: []string{},
onlyRWApiKeys: false,
onlyRWApiKey: false,
}
}
@@ -67,8 +67,8 @@ func (m *AuthenticateMiddleware) WithRedirectErrorMessage(message string) *Authe
return m
}
func (m *AuthenticateMiddleware) WithOnlyRWApiKeys(onlyRW bool) *AuthenticateMiddleware {
m.onlyRWApiKeys = onlyRW
func (m *AuthenticateMiddleware) WithOnlyRWApiKey(onlyRW bool) *AuthenticateMiddleware {
m.onlyRWApiKey = onlyRW
return m
}
@@ -145,7 +145,7 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKeyHeader(r *http.Request) (*mod
var user *models.User
userKey := strings.TrimSpace(key)
user, err = m.userSrvc.GetUserByKey(userKey)
user, err = m.userSrvc.GetUserByKey(userKey, m.onlyRWApiKey)
if err != nil {
return nil, err
}
@@ -159,7 +159,7 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKeyQuery(r *http.Request) (*mode
if userKey == "" {
return nil, errEmptyKey
}
user, err := m.userSrvc.GetUserByKey(userKey)
user, err := m.userSrvc.GetUserByKey(userKey, m.onlyRWApiKey)
if err != nil {
return nil, err
}

View File

@@ -46,14 +46,6 @@ func (m *UserServiceMock) GetUserByOidc(s1, s2 string) (*models.User, error) {
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetUserByRWKey(s string) (*models.User, error) {
args := m.Called(s)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetAll() ([]*models.User, error) {
args := m.Called()
return args.Get(0).([]*models.User), args.Error(1)

View File

@@ -1,10 +1,9 @@
package models
type ApiKey struct {
ID uint `json:"id" gorm:"primary_key"`
ApiKey string `json:"api_key" gorm:"primary_key"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
UserID string `json:"-" gorm:"not null; index:idx_api_key_user"`
ApiKey string `json:"api_key" gorm:"unique"`
ReadOnly bool `json:"readonly" gorm:"default:false"`
Label string `json:"label" gorm:"type:varchar(64)"`
}

View File

@@ -33,7 +33,6 @@ type SettingsVMCombinedLabel struct {
}
type SettingsApiKeys struct {
ID uint
Name string
Value string
ReadOnly bool

View File

@@ -26,25 +26,9 @@ func (r *ApiKeyRepository) GetAll() ([]*models.ApiKey, error) {
return keys, nil
}
func (r *ApiKeyRepository) GetById(id uint) (*models.ApiKey, error) {
func (r *ApiKeyRepository) GetByApiKey(apiKey string, readOnly bool) (*models.ApiKey, error) {
key := &models.ApiKey{}
if err := r.db.Where(&models.ApiKey{ID: id}).First(key).Error; err != nil {
return key, err
}
return key, nil
}
func (r *ApiKeyRepository) GetByApiKey(apiKey string) (*models.ApiKey, error) {
key := &models.ApiKey{}
if err := r.db.Where(&models.ApiKey{ApiKey: apiKey}).First(key).Error; err != nil {
return key, err
}
return key, nil
}
func (r *ApiKeyRepository) GetByRWApiKey(apiKey string) (*models.ApiKey, error) {
key := &models.ApiKey{}
if err := r.db.Where(&models.ApiKey{ApiKey: apiKey, ReadOnly: false}).First(key).Error; err != nil {
if err := r.db.Where(&models.ApiKey{ApiKey: apiKey, ReadOnly: readOnly}).First(key).Error; err != nil {
return key, err
}
return key, nil
@@ -74,8 +58,8 @@ func (r *ApiKeyRepository) Insert(key *models.ApiKey) (*models.ApiKey, error) {
return key, nil
}
func (r *ApiKeyRepository) Delete(id uint) error {
func (r *ApiKeyRepository) Delete(apiKey string) error {
return r.db.
Where("id = ?", id).
Where("api_key = ?", apiKey).
Delete(models.ApiKey{}).Error
}

View File

@@ -147,10 +147,8 @@ type ILeaderboardRepository interface {
type IApiKeyRepository interface {
IBaseRepository
GetAll() ([]*models.ApiKey, error)
GetById(uint) (*models.ApiKey, error)
GetByUser(string) ([]*models.ApiKey, error)
GetByApiKey(string) (*models.ApiKey, error)
GetByRWApiKey(string) (*models.ApiKey, error)
GetByApiKey(string, bool) (*models.ApiKey, error)
Insert(*models.ApiKey) (*models.ApiKey, error)
Delete(uint) error
Delete(string) error
}

View File

@@ -37,7 +37,7 @@ func NewHeartbeatApiHandler(userService services.IUserService, heartbeatService
func (h *HeartbeatApiHandler) RegisterRoutes(router chi.Router) {
router.Group(func(r chi.Router) {
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).WithOptionalForMethods(http.MethodOptions).WithOnlyRWApiKeys(true).Handler,
middlewares.NewAuthenticateMiddleware(h.userSrvc).WithOptionalForMethods(http.MethodOptions).WithOnlyRWApiKey(true).Handler,
customMiddleware.NewWakatimeRelayMiddleware().Handler,
)
// see https://github.com/muety/wakapi/issues/203

View File

@@ -891,9 +891,9 @@ func (h *SettingsHandler) actionAddApiKey(w http.ResponseWriter, r *http.Request
if _, err := h.apiKeySrvc.Create(&models.ApiKey{
User: middlewares.GetPrincipal(r),
Label: r.PostFormValue("api-name"),
Label: r.PostFormValue("api_name"),
ApiKey: apiKey,
ReadOnly: r.PostFormValue("api-readonly") == "true",
ReadOnly: r.PostFormValue("api_readonly") == "true",
}); err != nil {
return actionResult{http.StatusInternalServerError, "", conf.ErrInternalServerError, nil}
}
@@ -1034,7 +1034,6 @@ func (h *SettingsHandler) buildViewModel(r *http.Request, w http.ResponseWriter,
// API keys
combinedApiKeys := []*view.SettingsApiKeys{
{
ID: 0,
Name: "Main API Key",
Value: user.ApiKey,
ReadOnly: false,
@@ -1053,7 +1052,6 @@ func (h *SettingsHandler) buildViewModel(r *http.Request, w http.ResponseWriter,
}
for _, apiKey := range apiKeys {
combinedApiKeys = append(combinedApiKeys, &view.SettingsApiKeys{
ID: apiKey.ID,
Name: apiKey.Label,
Value: apiKey.ApiKey,
ReadOnly: apiKey.ReadOnly,

View File

@@ -2,6 +2,7 @@ package services
import (
"errors"
"strings"
"time"
"github.com/leandro-lugaresi/hub"
@@ -20,67 +21,83 @@ type ApiKeyService struct {
}
func NewApiKeyService(apiKeyRepository repositories.IApiKeyRepository) *ApiKeyService {
return &ApiKeyService{
srv := &ApiKeyService{
config: config.Get(),
eventBus: config.EventBus(),
repository: apiKeyRepository,
cache: cache.New(24*time.Hour, 24*time.Hour),
}
onApiKeyCreate := srv.eventBus.Subscribe(0, config.EventApiKeyCreate)
go func(sub *hub.Subscription) {
for m := range sub.Receiver {
srv.invalidateUserCache(m.Fields[config.FieldUserId].(string))
}
}(&onApiKeyCreate)
onApiKeyDelete := srv.eventBus.Subscribe(0, config.EventApiKeyDelete)
go func(sub *hub.Subscription) {
for m := range sub.Receiver {
srv.invalidateUserCache(m.Fields[config.FieldUserId].(string))
}
}(&onApiKeyDelete)
return srv
}
func (srv *ApiKeyService) GetById(id uint) (*models.ApiKey, error) {
return srv.repository.GetById(id)
}
func (srv *ApiKeyService) GetByApiKey(apiKey string) (*models.ApiKey, error) {
return srv.repository.GetByApiKey(apiKey)
}
func (srv *ApiKeyService) GetByRWApiKey(apiKey string) (*models.ApiKey, error) {
return srv.repository.GetByRWApiKey(apiKey)
func (srv *ApiKeyService) GetByApiKey(apiKey string, readOnly bool) (*models.ApiKey, error) {
return srv.repository.GetByApiKey(apiKey, readOnly)
}
func (srv *ApiKeyService) GetByUser(userId string) ([]*models.ApiKey, error) {
if labels, found := srv.cache.Get(userId); found {
return labels.([]*models.ApiKey), nil
if userApiKeys, found := srv.cache.Get(userId); found {
return userApiKeys.([]*models.ApiKey), nil
}
labels, err := srv.repository.GetByUser(userId)
userApiKeys, err := srv.repository.GetByUser(userId)
if err != nil {
return nil, err
}
srv.cache.Set(userId, labels, cache.DefaultExpiration)
return labels, nil
srv.cache.Set(userId, userApiKeys, cache.DefaultExpiration)
return userApiKeys, nil
}
func (srv *ApiKeyService) Create(label *models.ApiKey) (*models.ApiKey, error) {
result, err := srv.repository.Insert(label)
func (srv *ApiKeyService) Create(apiKey *models.ApiKey) (*models.ApiKey, error) {
result, err := srv.repository.Insert(apiKey)
if err != nil {
return nil, err
}
srv.cache.Delete(result.UserID)
srv.notifyUpdate(label, false)
srv.notifyUpdate(apiKey, false)
return result, nil
}
func (srv *ApiKeyService) Delete(label *models.ApiKey) error {
if label.UserID == "" {
func (srv *ApiKeyService) Delete(apiKey *models.ApiKey) error {
if apiKey.UserID == "" {
return errors.New("no user id specified")
}
err := srv.repository.Delete(label.ID)
srv.cache.Delete(label.UserID)
srv.notifyUpdate(label, true)
err := srv.repository.Delete(apiKey.ApiKey)
srv.cache.Delete(apiKey.UserID)
srv.notifyUpdate(apiKey, true)
return err
}
func (srv *ApiKeyService) notifyUpdate(label *models.ApiKey, isDelete bool) {
func (srv *ApiKeyService) notifyUpdate(apiKey *models.ApiKey, isDelete bool) {
name := config.EventApiKeyCreate
if isDelete {
name = config.EventApiKeyDelete
}
srv.eventBus.Publish(hub.Message{
Name: name,
Fields: map[string]interface{}{config.FieldPayload: label, config.FieldUserId: label.UserID},
Fields: map[string]interface{}{config.FieldPayload: apiKey, config.FieldUserId: apiKey.UserID},
})
}
func (srv *ApiKeyService) invalidateUserCache(userId string) {
for key := range srv.cache.Items() {
if strings.Contains(key, userId) {
srv.cache.Delete(key)
}
}
}

View File

@@ -148,8 +148,7 @@ type ILeaderboardService interface {
type IUserService interface {
GetUserById(string) (*models.User, error)
GetUserByKey(string) (*models.User, error)
GetUserByRWKey(string) (*models.User, error)
GetUserByKey(string, bool) (*models.User, error)
GetUserByEmail(string) (*models.User, error)
GetUserByResetToken(string) (*models.User, error)
GetUserByUnsubscribeToken(string) (*models.User, error)
@@ -177,9 +176,7 @@ type IUserService interface {
}
type IApiKeyService interface {
GetById(uint) (*models.ApiKey, error)
GetByApiKey(string) (*models.ApiKey, error)
GetByRWApiKey(string) (*models.ApiKey, error)
GetByApiKey(string, bool) (*models.ApiKey, error)
GetByUser(string) ([]*models.ApiKey, error)
Create(*models.ApiKey) (*models.ApiKey, error)
Delete(*models.ApiKey) error

View File

@@ -99,7 +99,7 @@ func (srv *UserService) GetUserById(userId string) (*models.User, error) {
return u, nil
}
func (srv *UserService) GetUserByKey(key string) (*models.User, error) {
func (srv *UserService) GetUserByKey(key string, onlyRWApiKey bool) (*models.User, error) {
if key == "" {
return nil, errors.New("key must not be empty")
}
@@ -114,31 +114,7 @@ func (srv *UserService) GetUserByKey(key string) (*models.User, error) {
return u, nil
}
apiKey, err := srv.apiKeyService.GetByApiKey(key)
if err == nil {
srv.cache.SetDefault(apiKey.User.ID, apiKey.User)
return apiKey.User, nil
}
return nil, err
}
func (srv *UserService) GetUserByRWKey(key string) (*models.User, error) {
if key == "" {
return nil, errors.New("key must not be empty")
}
if u, ok := srv.cache.Get(key); ok {
return u.(*models.User), nil
}
u, err := srv.repository.FindOne(models.User{ApiKey: key})
if err == nil {
srv.cache.SetDefault(u.ID, u)
return u, nil
}
apiKey, err := srv.apiKeyService.GetByApiKey(key)
apiKey, err := srv.apiKeyService.GetByApiKey(key, !onlyRWApiKey)
if err == nil {
srv.cache.SetDefault(apiKey.User.ID, apiKey.User)
return apiKey.User, nil

View File

@@ -932,13 +932,13 @@
<div class="flex flex-col gap-2 items-end">
<input class="appearance-none bg-card text-foreground outline-none rounded py-2 px-4 focus:bg-focused"
type="text" id="api-name" name="api-name" placeholder="Key Name" minlength="1"
type="text" id="api-name" name="api_name" placeholder="Key Name" minlength="1"
maxlength="64" required>
<div class="flex gap-2 items-center justify-end mt-4 float-left">
<label class="font-semibold text-foreground" for="api-readonly">Read Only</label>
<div>
<select autocomplete="off" id="api-readonly" name="api-readonly" class="select-default grow">
<select autocomplete="off" id="api-readonly" name="api_readonly" class="select-default grow">
<option value="false" class="cursor-pointer">No</option>
<option value="true" class="cursor-pointer">Yes</option>
</select>
@@ -971,7 +971,7 @@
<td class="py-2 text-center">
{{ if $ApiKey.ReadOnly }}
<span class=" rounded-full text-xs">
Read Only
Read-Only
</span>
{{ else }}
<span class="rounded-full text-xs text-accent">