Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-db/go/personal_access_token.go
2498 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package db
6
7
import (
8
"context"
9
"database/sql/driver"
10
"errors"
11
"fmt"
12
"strings"
13
"time"
14
15
"github.com/google/uuid"
16
"gorm.io/gorm"
17
)
18
19
type PersonalAccessToken struct {
20
ID uuid.UUID `gorm:"primary_key;column:id;type:varchar;size:255;" json:"id"`
21
UserID uuid.UUID `gorm:"column:userId;type:varchar;size:255;" json:"userId"`
22
Hash string `gorm:"column:hash;type:varchar;size:255;" json:"hash"`
23
Name string `gorm:"column:name;type:varchar;size:255;" json:"name"`
24
Scopes Scopes `gorm:"column:scopes;type:text;size:65535;" json:"scopes"`
25
ExpirationTime time.Time `gorm:"column:expirationTime;type:timestamp;" json:"expirationTime"`
26
CreatedAt time.Time `gorm:"column:createdAt;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"createdAt"`
27
LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"`
28
29
// deleted is reserved for use by periodic deleter.
30
_ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"`
31
}
32
33
// TableName sets the insert table name for this struct type
34
func (d *PersonalAccessToken) TableName() string {
35
return "d_b_personal_access_token"
36
}
37
38
func GetPersonalAccessTokenForUser(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID) (PersonalAccessToken, error) {
39
var token PersonalAccessToken
40
41
if tokenID == uuid.Nil {
42
return PersonalAccessToken{}, fmt.Errorf("Token ID is a required argument to get personal access token for user")
43
}
44
45
if userID == uuid.Nil {
46
return PersonalAccessToken{}, fmt.Errorf("User ID is a required argument to get personal access token for user")
47
}
48
49
tx := conn.
50
WithContext(ctx).
51
Where("id = ?", tokenID).
52
Where("userId = ?", userID).
53
Where("deleted = ?", 0).
54
First(&token)
55
if tx.Error != nil {
56
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
57
return PersonalAccessToken{}, fmt.Errorf("Token with ID %s does not exist: %w", tokenID, ErrorNotFound)
58
}
59
return PersonalAccessToken{}, fmt.Errorf("Failed to retrieve token: %v", tx.Error)
60
}
61
62
return token, nil
63
}
64
65
func CreatePersonalAccessToken(ctx context.Context, conn *gorm.DB, req PersonalAccessToken) (PersonalAccessToken, error) {
66
if req.UserID == uuid.Nil {
67
return PersonalAccessToken{}, fmt.Errorf("Invalid or empty userID")
68
}
69
if req.Hash == "" {
70
return PersonalAccessToken{}, fmt.Errorf("Token hash required")
71
}
72
if req.Name == "" {
73
return PersonalAccessToken{}, fmt.Errorf("Token name required")
74
}
75
if req.ExpirationTime.IsZero() {
76
return PersonalAccessToken{}, fmt.Errorf("Expiration time required")
77
}
78
79
now := time.Now().UTC()
80
token := PersonalAccessToken{
81
ID: req.ID,
82
UserID: req.UserID,
83
Hash: req.Hash,
84
Name: req.Name,
85
Scopes: req.Scopes,
86
ExpirationTime: req.ExpirationTime,
87
CreatedAt: now,
88
LastModified: now,
89
}
90
91
tx := conn.WithContext(ctx).Create(req)
92
if tx.Error != nil {
93
return PersonalAccessToken{}, fmt.Errorf("Failed to create personal access token for user %s", req.UserID)
94
}
95
96
return token, nil
97
}
98
99
func UpdatePersonalAccessTokenHash(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID, hash string, expirationTime time.Time) (PersonalAccessToken, error) {
100
if tokenID == uuid.Nil {
101
return PersonalAccessToken{}, fmt.Errorf("Invalid or empty tokenID")
102
}
103
if userID == uuid.Nil {
104
return PersonalAccessToken{}, fmt.Errorf("Invalid or empty userID")
105
}
106
if hash == "" {
107
return PersonalAccessToken{}, fmt.Errorf("Token hash required")
108
}
109
if expirationTime.IsZero() {
110
return PersonalAccessToken{}, fmt.Errorf("Expiration time required")
111
}
112
113
db := conn.WithContext(ctx)
114
115
err := db.
116
Where("id = ?", tokenID).
117
Where("userId = ?", userID).
118
Where("deleted = ?", 0).
119
Select("hash", "expirationTime").Updates(PersonalAccessToken{Hash: hash, ExpirationTime: expirationTime}).
120
Error
121
if err != nil {
122
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
123
return PersonalAccessToken{}, fmt.Errorf("Token with ID %s does not exist: %w", tokenID, ErrorNotFound)
124
}
125
return PersonalAccessToken{}, fmt.Errorf("Failed to update token: %v", db.Error)
126
}
127
128
return GetPersonalAccessTokenForUser(ctx, conn, tokenID, userID)
129
}
130
131
func DeletePersonalAccessTokenForUser(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID) (int64, error) {
132
if tokenID == uuid.Nil {
133
return 0, fmt.Errorf("Invalid or empty tokenID")
134
}
135
136
if userID == uuid.Nil {
137
return 0, fmt.Errorf("Invalid or empty userID")
138
}
139
140
db := conn.WithContext(ctx)
141
142
db = db.
143
Table((&PersonalAccessToken{}).TableName()).
144
Where("id = ?", tokenID).
145
Where("userId = ?", userID).
146
Where("deleted = ?", 0).
147
Update("deleted", 1)
148
if db.Error != nil {
149
return 0, fmt.Errorf("failed to delete token (ID: %s): %v", tokenID.String(), db.Error)
150
}
151
152
if db.RowsAffected == 0 {
153
return 0, fmt.Errorf("token (ID: %s) for user (ID: %s) does not exist: %w", tokenID, userID, ErrorNotFound)
154
}
155
156
return db.RowsAffected, nil
157
}
158
159
func ListPersonalAccessTokensForUser(ctx context.Context, conn *gorm.DB, userID uuid.UUID, pagination Pagination) (*PaginatedResult[PersonalAccessToken], error) {
160
if userID == uuid.Nil {
161
return nil, fmt.Errorf("user ID is a required argument to list personal access tokens for user, got nil")
162
}
163
164
var results []PersonalAccessToken
165
166
tx := conn.
167
WithContext(ctx).
168
Table((&PersonalAccessToken{}).TableName()).
169
Where("userId = ?", userID).
170
Where("deleted = ?", 0).
171
Order("createdAt").
172
Scopes(Paginate(pagination)).
173
Find(&results)
174
if tx.Error != nil {
175
return nil, fmt.Errorf("failed to list personal access tokens for user %s: %w", userID.String(), tx.Error)
176
}
177
178
var count int64
179
tx = conn.
180
WithContext(ctx).
181
Table((&PersonalAccessToken{}).TableName()).
182
Where("userId = ?", userID).
183
Where("deleted = ?", 0).
184
Count(&count)
185
if tx.Error != nil {
186
return nil, fmt.Errorf("failed to count total number of personal access tokens for user %s: %w", userID.String(), tx.Error)
187
}
188
189
return &PaginatedResult[PersonalAccessToken]{
190
Results: results,
191
Total: count,
192
}, nil
193
}
194
195
type UpdatePersonalAccessTokenOpts struct {
196
TokenID uuid.UUID
197
UserID uuid.UUID
198
Name *string
199
Scopes *Scopes
200
}
201
202
func UpdatePersonalAccessTokenForUser(ctx context.Context, conn *gorm.DB, opts UpdatePersonalAccessTokenOpts) (PersonalAccessToken, error) {
203
if opts.TokenID == uuid.Nil {
204
return PersonalAccessToken{}, errors.New("Token ID is required to udpate personal access token for user")
205
}
206
if opts.UserID == uuid.Nil {
207
return PersonalAccessToken{}, errors.New("User ID is required to udpate personal access token for user")
208
}
209
210
var cols []string
211
update := PersonalAccessToken{}
212
if opts.Name != nil {
213
cols = append(cols, "name")
214
update.Name = *opts.Name
215
}
216
217
if opts.Scopes != nil {
218
cols = append(cols, "scopes")
219
update.Scopes = *opts.Scopes
220
}
221
222
if len(cols) == 0 {
223
return GetPersonalAccessTokenForUser(ctx, conn, opts.TokenID, opts.UserID)
224
}
225
226
tx := conn.
227
WithContext(ctx).
228
Table((&PersonalAccessToken{}).TableName()).
229
Where("id = ?", opts.TokenID).
230
Where("userId = ?", opts.UserID).
231
Where("deleted = ?", 0).
232
Select(cols).
233
Updates(update)
234
if tx.Error != nil {
235
return PersonalAccessToken{}, fmt.Errorf("failed to update personal access token: %w", tx.Error)
236
}
237
238
return GetPersonalAccessTokenForUser(ctx, conn, opts.TokenID, opts.UserID)
239
}
240
241
type Scopes []string
242
243
// Scan() and Value() allow having a list of strings as a type for Scopes
244
func (s *Scopes) Scan(src any) error {
245
bytes, ok := src.([]byte)
246
if !ok {
247
return errors.New("src value cannot cast to []byte")
248
}
249
250
if len(bytes) == 0 {
251
*s = nil
252
return nil
253
}
254
255
*s = strings.Split(string(bytes), ",")
256
return nil
257
}
258
259
func (s Scopes) Value() (driver.Value, error) {
260
if len(s) == 0 {
261
return "", nil
262
}
263
return strings.Join(s, ","), nil
264
}
265
266