package core
import (
"crypto/sha256"
"errors"
"fmt"
"regexp"
"sort"
"strings"
"time"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)
const (
tokenConfigKeyPrefix = "git-bug.token"
tokenValueKey = "value"
tokenTargetKey = "target"
tokenCreateTimeKey = "createtime"
)
var ErrTokenNotExist = errors.New("token doesn't exist")
func NewErrMultipleMatchToken(matching []entity.Id) *entity.ErrMultipleMatch {
return entity.NewErrMultipleMatch("token", matching)
}
// Token holds an API access token data
type Token struct {
Value string
Target string
CreateTime time.Time
}
// NewToken instantiate a new token
func NewToken(value, target string) *Token {
return &Token{
Value: value,
Target: target,
CreateTime: time.Now(),
}
}
func (t *Token) ID() entity.Id {
sum := sha256.Sum256([]byte(t.Target + t.Value))
return entity.Id(fmt.Sprintf("%x", sum))
}
// Validate ensure token important fields are valid
func (t *Token) Validate() error {
if t.Value == "" {
return fmt.Errorf("missing value")
}
if t.Target == "" {
return fmt.Errorf("missing target")
}
if t.CreateTime.IsZero() || t.CreateTime.Equal(time.Time{}) {
return fmt.Errorf("missing creation time")
}
if !TargetExist(t.Target) {
return fmt.Errorf("unknown target")
}
return nil
}
// LoadToken loads a token from the repo config
func LoadToken(repo repository.RepoCommon, id entity.Id) (*Token, error) {
keyPrefix := fmt.Sprintf("git-bug.token.%s.", id)
// read token config pairs
rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
if err != nil {
// Not exactly right due to the limitation of ReadAll()
return nil, ErrTokenNotExist
}
// trim key prefix
configs := make(map[string]string)
for key, value := range rawconfigs {
newKey := strings.TrimPrefix(key, keyPrefix)
configs[newKey] = value
}
token := &Token{}
token.Value = configs[tokenValueKey]
token.Target = configs[tokenTargetKey]
if createTime, ok := configs[tokenCreateTimeKey]; ok {
if t, err := repository.ParseTimestamp(createTime); err == nil {
token.CreateTime = t
}
}
return token, nil
}
// LoadTokenPrefix load a token from the repo config with a prefix
func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) {
tokens, err := ListTokens(repo)
if err != nil {
return nil, err
}
// preallocate but empty
matching := make([]entity.Id, 0, 5)
for _, id := range tokens {
if id.HasPrefix(prefix) {
matching = append(matching, id)
}
}
if len(matching) > 1 {
return nil, NewErrMultipleMatchToken(matching)
}
if len(matching) == 0 {
return nil, ErrTokenNotExist
}
return LoadToken(repo, matching[0])
}
// ListTokens list all existing token ids
func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".")
if err != nil {
return nil, err
}
re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`)
if err != nil {
panic(err)
}
set := make(map[string]interface{})
for key := range configs {
res := re.FindStringSubmatch(key)
if res == nil {
continue
}
set[res[1]] = nil
}
result := make([]entity.Id, 0, len(set))
for key := range set {
result = append(result, entity.Id(key))
}
sort.Sort(entity.Alphabetical(result))
return result, nil
}
// ListTokensWithTarget list all token ids associated with the target
func ListTokensWithTarget(repo repository.RepoCommon, target string) ([]entity.Id, error) {
var ids []entity.Id
tokensIds, err := ListTokens(repo)
if err != nil {
return nil, err
}
for _, tokenId := range tokensIds {
token, err := LoadToken(repo, tokenId)
if err != nil {
return nil, err
}
if token.Target == target {
ids = append(ids, tokenId)
}
}
return ids, nil
}
// LoadTokens load all existing tokens
func LoadTokens(repo repository.RepoCommon) ([]*Token, error) {
tokensIds, err := ListTokens(repo)
if err != nil {
return nil, err
}
var tokens []*Token
for _, id := range tokensIds {
token, err := LoadToken(repo, id)
if err != nil {
return nil, err
}
tokens = append(tokens, token)
}
return tokens, nil
}
// LoadTokensWithTarget load all existing tokens for a given target
func LoadTokensWithTarget(repo repository.RepoCommon, target string) ([]*Token, error) {
tokensIds, err := ListTokens(repo)
if err != nil {
return nil, err
}
var tokens []*Token
for _, id := range tokensIds {
token, err := LoadToken(repo, id)
if err != nil {
return nil, err
}
if token.Target == target {
tokens = append(tokens, token)
}
}
return tokens, nil
}
// TokenIdExist return wether token id exist or not
func TokenIdExist(repo repository.RepoCommon, id entity.Id) bool {
_, err := LoadToken(repo, id)
return err == nil
}
// TokenExist return wether there is a token with a certain value or not
func TokenExist(repo repository.RepoCommon, value string) bool {
tokens, err := LoadTokens(repo)
if err != nil {
return false
}
for _, token := range tokens {
if token.Value == value {
return true
}
}
return false
}
// TokenExistWithTarget same as TokenExist but restrict search for a given target
func TokenExistWithTarget(repo repository.RepoCommon, value string, target string) bool {
tokens, err := LoadTokensWithTarget(repo, target)
if err != nil {
return false
}
for _, token := range tokens {
if token.Value == value {
return true
}
}
return false
}
// StoreToken stores a token in the repo config
func StoreToken(repo repository.RepoCommon, token *Token) error {
storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey)
err := repo.GlobalConfig().StoreString(storeValueKey, token.Value)
if err != nil {
return err
}
storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenTargetKey)
err = repo.GlobalConfig().StoreString(storeTargetKey, token.Target)
if err != nil {
return err
}
createTimeKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenCreateTimeKey)
return repo.GlobalConfig().StoreTimestamp(createTimeKey, token.CreateTime)
}
// RemoveToken removes a token from the repo config
func RemoveToken(repo repository.RepoCommon, id entity.Id) error {
keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
return repo.GlobalConfig().RemoveAll(keyPrefix)
}
// LoadOrCreateToken will try to load a token matching the same value or create it
func LoadOrCreateToken(repo repository.RepoCommon, target, tokenValue string) (*Token, error) {
tokens, err := LoadTokensWithTarget(repo, target)
if err != nil {
return nil, err
}
for _, token := range tokens {
if token.Value == tokenValue {
return token, nil
}
}
token := NewToken(tokenValue, target)
err = StoreToken(repo, token)
if err != nil {
return nil, err
}
return token, nil
}