diff --git a/pkg/db/api-key.go b/pkg/db/api-key.go index e0b8988b2..7ad78677d 100644 --- a/pkg/db/api-key.go +++ b/pkg/db/api-key.go @@ -4,6 +4,7 @@ import "github.com/ncarlier/reader/pkg/model" // APIKeyRepository is the repository interface to manage API keys type APIKeyRepository interface { + GetAPIKeyByID(id uint) (*model.APIKey, error) GetAPIKeyByToken(token string) (*model.APIKey, error) GetAPIKeyByUserIDAndAlias(userID uint, alias string) (*model.APIKey, error) GetAPIKeysByUserID(userID uint) ([]model.APIKey, error) diff --git a/pkg/db/postgres/api-key.go b/pkg/db/postgres/api-key.go index 27d56aa6e..63a260a9d 100644 --- a/pkg/db/postgres/api-key.go +++ b/pkg/db/postgres/api-key.go @@ -3,20 +3,22 @@ package postgres import ( "database/sql" "errors" + "fmt" "github.com/ncarlier/reader/pkg/model" ) -func (pg *DB) createAPIKey(apiKey model.APIKey) (*model.APIKey, error) { - row := pg.db.QueryRow(` - INSERT INTO api_keys - (user_id, alias, token) - VALUES - ($1, $2, $3) - RETURNING id, user_id, alias, token, created_at - `, - apiKey.UserID, apiKey.Alias, apiKey.Token, - ) +const apiKeyColumns = ` + id, + user_id, + alias, + token, + last_usage_at, + created_at, + updated_at +` + +func scanAPIKeyRow(row *sql.Row) (*model.APIKey, error) { result := model.APIKey{} err := row.Scan( @@ -24,40 +26,44 @@ func (pg *DB) createAPIKey(apiKey model.APIKey) (*model.APIKey, error) { &result.UserID, &result.Alias, &result.Token, + &result.LastUsageAt, &result.CreatedAt, + &result.UpdatedAt, ) - if err != nil { + + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { return nil, err } return &result, nil } +func (pg *DB) createAPIKey(apiKey model.APIKey) (*model.APIKey, error) { + row := pg.db.QueryRow(fmt.Sprintf(` + INSERT INTO api_keys + (user_id, alias, token) + VALUES + ($1, $2, $3) + RETURNING %s + `, apiKeyColumns), + apiKey.UserID, apiKey.Alias, apiKey.Token, + ) + return scanAPIKeyRow(row) +} + func (pg *DB) updateAPIKey(apiKey model.APIKey) (*model.APIKey, error) { - row := pg.db.QueryRow(` + row := pg.db.QueryRow(fmt.Sprintf(` UPDATE api_keys SET alias=$3, updated_at=NOW() WHERE id=$1 AND user_id=$2 - RETURNING id, user_id, alias, token, last_usage_at, created_at, updated_at - `, + RETURNING %s + `, apiKeyColumns), apiKey.ID, apiKey.UserID, apiKey.Alias, ) - result := model.APIKey{} - - err := row.Scan( - &result.ID, - &result.UserID, - &result.Alias, - &result.Token, - &result.LastUsageAt, - &result.CreatedAt, - &result.UpdatedAt, - ) - if err != nil { - return nil, err - } - return &result, nil + return scanAPIKeyRow(row) } // CreateOrUpdateAPIKey creates or updates an apiKey into the DB @@ -68,86 +74,50 @@ func (pg *DB) CreateOrUpdateAPIKey(apiKey model.APIKey) (*model.APIKey, error) { return pg.createAPIKey(apiKey) } +// GetAPIKeyByID get an apiKey from the DB +func (pg *DB) GetAPIKeyByID(id uint) (*model.APIKey, error) { + row := pg.db.QueryRow(fmt.Sprintf(` + SELECT %s + FROM api_keys + WHERE id = $1`, apiKeyColumns), + id, + ) + return scanAPIKeyRow(row) +} + // GetAPIKeyByToken find an apiKey by token form the DB (last usage is updated!) func (pg *DB) GetAPIKeyByToken(token string) (*model.APIKey, error) { - row := pg.db.QueryRow(` + row := pg.db.QueryRow(fmt.Sprintf(` UPDATE api_keys SET last_usage_at=NOW() WHERE token=$1 - RETURNING id, user_id, alias, token, last_usage_at, created_at, updated_at - `, + RETURNING %s + `, apiKeyColumns), token, ) - result := model.APIKey{} - - err := row.Scan( - &result.ID, - &result.UserID, - &result.Alias, - &result.Token, - &result.LastUsageAt, - &result.CreatedAt, - &result.UpdatedAt, - ) - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - return &result, nil + return scanAPIKeyRow(row) } // GetAPIKeyByUserIDAndAlias returns API key of an user by its alias func (pg *DB) GetAPIKeyByUserIDAndAlias(userID uint, alias string) (*model.APIKey, error) { - row := pg.db.QueryRow(` - SELECT - id, - user_id, - alias, - token, - last_usage_at, - created_at, - updated_at + row := pg.db.QueryRow(fmt.Sprintf(` + SELECT %s FROM api_keys - WHERE user_id = $1 AND alias = $2`, + WHERE user_id = $1 AND alias = $2`, apiKeyColumns), userID, alias, ) - result := model.APIKey{} - - err := row.Scan( - &result.ID, - &result.UserID, - &result.Alias, - &result.Token, - &result.LastUsageAt, - &result.CreatedAt, - &result.UpdatedAt, - ) - - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - return &result, nil + return scanAPIKeyRow(row) } // GetAPIKeysByUserID returns api-keys of an user from DB func (pg *DB) GetAPIKeysByUserID(userID uint) ([]model.APIKey, error) { - rows, err := pg.db.Query(` - SELECT - id, - user_id, - alias, - token, - last_usage_at, - created_at, - updated_at + rows, err := pg.db.Query(fmt.Sprintf(` + SELECT %s FROM api_keys WHERE user_id=$1 - ORDER BY alias ASC`, + ORDER BY alias ASC`, apiKeyColumns), userID, ) if err != nil { diff --git a/pkg/schema/api-key.go b/pkg/schema/api-key.go new file mode 100644 index 000000000..68d29db1f --- /dev/null +++ b/pkg/schema/api-key.go @@ -0,0 +1,105 @@ +package schema + +import ( + "errors" + + "github.com/ncarlier/reader/pkg/tooling" + + "github.com/graphql-go/graphql" + "github.com/ncarlier/reader/pkg/service" +) + +var apiKeyType = graphql.NewObject( + graphql.ObjectConfig{ + Name: "APIKey", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.Int, + }, + "alias": &graphql.Field{ + Type: graphql.String, + }, + "token": &graphql.Field{ + Type: graphql.String, + }, + "last_usage_at": &graphql.Field{ + Type: graphql.DateTime, + }, + "created_at": &graphql.Field{ + Type: graphql.DateTime, + }, + "updated_at": &graphql.Field{ + Type: graphql.DateTime, + }, + }, + }, +) + +// QUERIES + +var apiKeysQueryField = &graphql.Field{ + Type: graphql.NewList(apiKeyType), + Resolve: apiKeysResolver, +} + +func apiKeysResolver(p graphql.ResolveParams) (interface{}, error) { + apiKeys, err := service.Lookup().GetAPIKeys(p.Context) + if err != nil { + return nil, err + } + return apiKeys, nil +} + +// MUTATIONS + +var createOrUpdateAPIKeyMutationField = &graphql.Field{ + Type: apiKeyType, + Description: "create or update an API key (use the ID parameter to update)", + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.ID, + }, + "alias": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: createOrUpdateAPIKeyResolver, +} + +func createOrUpdateAPIKeyResolver(p graphql.ResolveParams) (interface{}, error) { + var id *uint + if val, ok := tooling.ConvGQLStringToUint(p.Args["id"]); ok { + id = &val + } + alias, _ := p.Args["alias"].(string) + + apiKey, err := service.Lookup().CreateOrUpdateAPIKey(p.Context, id, alias) + if err != nil { + return nil, err + } + return apiKey, nil +} + +var deleteAPIKeyMutationField = &graphql.Field{ + Type: apiKeyType, + Description: "delete an API key", + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.ID), + }, + }, + Resolve: deleteAPIKeyResolver, +} + +func deleteAPIKeyResolver(p graphql.ResolveParams) (interface{}, error) { + id, ok := tooling.ConvGQLStringToUint(p.Args["id"]) + if !ok { + return nil, errors.New("invalid API key ID") + } + + apiKey, err := service.Lookup().DeleteAPIKey(p.Context, id) + if err != nil { + return nil, err + } + return apiKey, nil +} diff --git a/pkg/schema/root.go b/pkg/schema/root.go index 51b88dab2..79df61302 100644 --- a/pkg/schema/root.go +++ b/pkg/schema/root.go @@ -14,6 +14,7 @@ var rootQuery = graphql.NewObject( Fields: graphql.Fields{ "me": meQueryField, "categories": categoriesQueryField, + "apiKeys": apiKeysQueryField, }, }, ) @@ -24,6 +25,8 @@ var rootMutation = graphql.NewObject( Fields: graphql.Fields{ "createOrUpdateCategory": createOrUpdateCategoryMutationField, "deleteCategory": deleteCategoryMutationField, + "createOrUpdateAPIKey": createOrUpdateAPIKeyMutationField, + "deleteAPIKey": deleteAPIKeyMutationField, }, }, ) diff --git a/pkg/service/api-keys.go b/pkg/service/api-keys.go index 1e0f3fe9b..ad25e83ed 100644 --- a/pkg/service/api-keys.go +++ b/pkg/service/api-keys.go @@ -1,6 +1,9 @@ package service import ( + "context" + "errors" + "github.com/ncarlier/reader/pkg/model" ) @@ -8,3 +11,65 @@ import ( func (reg *Registry) GetAPIKeyByToken(token string) (*model.APIKey, error) { return reg.db.GetAPIKeyByToken(token) } + +// GetAPIKeys get API keys from current user +func (reg *Registry) GetAPIKeys(ctx context.Context) (*[]model.APIKey, error) { + uid := getCurrentUserFromContext(ctx) + + apiKeys, err := reg.db.GetAPIKeysByUserID(uid) + if err != nil { + reg.logger.Info().Err(err).Uint( + "uid", uid, + ).Msg("unable to get API keys") + return nil, err + } + + return &apiKeys, err +} + +// CreateOrUpdateAPIKey create or update an API key for current user +func (reg *Registry) CreateOrUpdateAPIKey(ctx context.Context, id *uint, alias string) (*model.APIKey, error) { + uid := getCurrentUserFromContext(ctx) + + builder := model.NewAPIKeyBuilder() + apiKey := builder.UserID(uid).Alias(alias).Build() + apiKey.ID = id + result, err := reg.db.CreateOrUpdateAPIKey(*apiKey) + if err != nil { + evt := reg.logger.Info().Err(err).Uint( + "uid", uid, + ).Str("alias", alias) + if id != nil { + evt.Uint("id", *id).Msg("unable to update API key") + } else { + evt.Msg("unable to create API key") + } + return nil, err + } + return result, err +} + +// DeleteAPIKey delete an API key of the current user +func (reg *Registry) DeleteAPIKey(ctx context.Context, id uint) (*model.APIKey, error) { + uid := getCurrentUserFromContext(ctx) + + apiKey, err := reg.db.GetAPIKeyByID(id) + if err != nil || apiKey == nil || apiKey.UserID != uid { + if err == nil { + err = errors.New("API key not found") + } + reg.logger.Info().Err(err).Uint( + "uid", uid, + ).Uint("id", id).Msg("unable to delete API key") + return nil, err + } + + err = reg.db.DeleteAPIKey(*apiKey) + if err != nil { + reg.logger.Info().Err(err).Uint( + "uid", uid, + ).Uint("id", id).Msg("unable to delete API key") + return nil, err + } + return apiKey, nil +}