mirror of
https://github.com/muety/wakapi.git
synced 2025-12-05 22:20:24 -08:00
feat: refactor api key service
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)"`
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ type SettingsVMCombinedLabel struct {
|
||||
}
|
||||
|
||||
type SettingsApiKeys struct {
|
||||
ID uint
|
||||
Name string
|
||||
Value string
|
||||
ReadOnly bool
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user