1package core
2
3import (
4 "crypto/sha256"
5 "errors"
6 "fmt"
7 "regexp"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/MichaelMure/git-bug/entity"
13 "github.com/MichaelMure/git-bug/repository"
14)
15
16const (
17 tokenConfigKeyPrefix = "git-bug.token"
18 tokenValueKey = "value"
19 tokenTargetKey = "target"
20 tokenCreateTimeKey = "createtime"
21)
22
23var ErrTokenNotExist = errors.New("token doesn't exist")
24
25func NewErrMultipleMatchToken(matching []entity.Id) *entity.ErrMultipleMatch {
26 return entity.NewErrMultipleMatch("token", matching)
27}
28
29// Token holds an API access token data
30type Token struct {
31 Value string
32 Target string
33 CreateTime time.Time
34}
35
36// NewToken instantiate a new token
37func NewToken(value, target string) *Token {
38 return &Token{
39 Value: value,
40 Target: target,
41 CreateTime: time.Now(),
42 }
43}
44
45func (t *Token) ID() entity.Id {
46 sum := sha256.Sum256([]byte(t.Target + t.Value))
47 return entity.Id(fmt.Sprintf("%x", sum))
48}
49
50// Validate ensure token important fields are valid
51func (t *Token) Validate() error {
52 if t.Value == "" {
53 return fmt.Errorf("missing value")
54 }
55 if t.Target == "" {
56 return fmt.Errorf("missing target")
57 }
58 if t.CreateTime.IsZero() || t.CreateTime.Equal(time.Time{}) {
59 return fmt.Errorf("missing creation time")
60 }
61 if !TargetExist(t.Target) {
62 return fmt.Errorf("unknown target")
63 }
64 return nil
65}
66
67// LoadToken loads a token from the repo config
68func LoadToken(repo repository.RepoCommon, id entity.Id) (*Token, error) {
69 keyPrefix := fmt.Sprintf("git-bug.token.%s.", id)
70
71 // read token config pairs
72 rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
73 if err != nil {
74 // Not exactly right due to the limitation of ReadAll()
75 return nil, ErrTokenNotExist
76 }
77
78 // trim key prefix
79 configs := make(map[string]string)
80 for key, value := range rawconfigs {
81 newKey := strings.TrimPrefix(key, keyPrefix)
82 configs[newKey] = value
83 }
84
85 token := &Token{}
86
87 token.Value = configs[tokenValueKey]
88 token.Target = configs[tokenTargetKey]
89 if createTime, ok := configs[tokenCreateTimeKey]; ok {
90 if t, err := repository.ParseTimestamp(createTime); err == nil {
91 token.CreateTime = t
92 }
93 }
94
95 return token, nil
96}
97
98// LoadTokenPrefix load a token from the repo config with a prefix
99func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) {
100 tokens, err := ListTokens(repo)
101 if err != nil {
102 return nil, err
103 }
104
105 // preallocate but empty
106 matching := make([]entity.Id, 0, 5)
107
108 for _, id := range tokens {
109 if id.HasPrefix(prefix) {
110 matching = append(matching, id)
111 }
112 }
113
114 if len(matching) > 1 {
115 return nil, NewErrMultipleMatchToken(matching)
116 }
117
118 if len(matching) == 0 {
119 return nil, ErrTokenNotExist
120 }
121
122 return LoadToken(repo, matching[0])
123}
124
125// ListTokens list all existing token ids
126func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
127 configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".")
128 if err != nil {
129 return nil, err
130 }
131
132 re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`)
133 if err != nil {
134 panic(err)
135 }
136
137 set := make(map[string]interface{})
138
139 for key := range configs {
140 res := re.FindStringSubmatch(key)
141
142 if res == nil {
143 continue
144 }
145
146 set[res[1]] = nil
147 }
148
149 result := make([]entity.Id, 0, len(set))
150 for key := range set {
151 result = append(result, entity.Id(key))
152 }
153
154 sort.Sort(entity.Alphabetical(result))
155
156 return result, nil
157}
158
159// ListTokensWithTarget list all token ids associated with the target
160func ListTokensWithTarget(repo repository.RepoCommon, target string) ([]entity.Id, error) {
161 var ids []entity.Id
162 tokensIds, err := ListTokens(repo)
163 if err != nil {
164 return nil, err
165 }
166
167 for _, tokenId := range tokensIds {
168 token, err := LoadToken(repo, tokenId)
169 if err != nil {
170 return nil, err
171 }
172
173 if token.Target == target {
174 ids = append(ids, tokenId)
175 }
176 }
177 return ids, nil
178}
179
180// LoadTokens load all existing tokens
181func LoadTokens(repo repository.RepoCommon) ([]*Token, error) {
182 tokensIds, err := ListTokens(repo)
183 if err != nil {
184 return nil, err
185 }
186
187 var tokens []*Token
188 for _, id := range tokensIds {
189 token, err := LoadToken(repo, id)
190 if err != nil {
191 return nil, err
192 }
193 tokens = append(tokens, token)
194 }
195 return tokens, nil
196}
197
198// TokenIdExist return wether token id exist or not
199func TokenIdExist(repo repository.RepoCommon, id entity.Id) bool {
200 _, err := LoadToken(repo, id)
201 return err == nil
202}
203
204// TokenExist return wether there is a token with a certain value or not
205func TokenExist(repo repository.RepoCommon, value string) bool {
206 tokens, err := LoadTokens(repo)
207 if err != nil {
208 return false
209 }
210 for _, token := range tokens {
211 if token.Value == value {
212 return true
213 }
214 }
215 return false
216}
217
218// TokenExistWithTarget same as TokenExist but restrict search for a given target
219func TokenExistWithTarget(repo repository.RepoCommon, value string, target string) bool {
220 tokens, err := LoadTokens(repo)
221 if err != nil {
222 return false
223 }
224 for _, token := range tokens {
225 if token.Value == value && token.Target == target {
226 return true
227 }
228 }
229 return false
230}
231
232// StoreToken stores a token in the repo config
233func StoreToken(repo repository.RepoCommon, token *Token) error {
234 storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey)
235 err := repo.GlobalConfig().StoreString(storeValueKey, token.Value)
236 if err != nil {
237 return err
238 }
239
240 storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenTargetKey)
241 err = repo.GlobalConfig().StoreString(storeTargetKey, token.Target)
242 if err != nil {
243 return err
244 }
245
246 createTimeKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenCreateTimeKey)
247 return repo.GlobalConfig().StoreTimestamp(createTimeKey, token.CreateTime)
248}
249
250// RemoveToken removes a token from the repo config
251func RemoveToken(repo repository.RepoCommon, id entity.Id) error {
252 keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
253 return repo.GlobalConfig().RemoveAll(keyPrefix)
254}