diff --git a/components/gitpod-db/go/personal_access_token.go b/components/gitpod-db/go/personal_access_token.go new file mode 100644 index 00000000000000..8119d794b9b976 --- /dev/null +++ b/components/gitpod-db/go/personal_access_token.go @@ -0,0 +1,102 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package db + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PersonalAccessToken struct { + ID uuid.UUID `gorm:"primary_key;column:id;type:varchar;size:255;" json:"id"` + UserID uuid.UUID `gorm:"column:userId;type:varchar;size:255;" json:"userId"` + Hash string `gorm:"column:hash;type:varchar;size:255;" json:"hash"` + Name string `gorm:"column:name;type:varchar;size:255;" json:"name"` + Description string `gorm:"column:description;type:varchar;size:255;" json:"description"` + Scopes Scopes `gorm:"column:scopes;type:text;size:65535;" json:"scopes"` + ExpirationTime time.Time `gorm:"column:expirationTime;type:timestamp;" json:"expirationTime"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"createdAt"` + LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"` + + // deleted is reserved for use by db-sync. + _ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"` +} + +type Scopes []string + +// TableName sets the insert table name for this struct type +func (d *PersonalAccessToken) TableName() string { + return "d_b_personal_access_token" +} + +func GetToken(ctx context.Context, conn *gorm.DB, id uuid.UUID) (PersonalAccessToken, error) { + var token PersonalAccessToken + + db := conn.WithContext(ctx) + + db = db.Where("id = ?", id).First(&token) + if db.Error != nil { + return PersonalAccessToken{}, fmt.Errorf("Failed to retrieve token: %w", db.Error) + } + + return token, nil +} + +func CreateToken(ctx context.Context, conn *gorm.DB, req PersonalAccessToken) (PersonalAccessToken, error) { + if req.UserID == uuid.Nil { + return PersonalAccessToken{}, fmt.Errorf("Invalid or empty userID") + } + if req.Hash == "" { + return PersonalAccessToken{}, fmt.Errorf("Token hash required") + } + if req.Name == "" { + return PersonalAccessToken{}, fmt.Errorf("Token name required") + } + if req.ExpirationTime.IsZero() { + return PersonalAccessToken{}, fmt.Errorf("Expiration time required") + } + + token := PersonalAccessToken{ + ID: req.ID, + UserID: req.UserID, + Hash: req.Hash, + Name: req.Name, + Description: req.Description, + Scopes: req.Scopes, + ExpirationTime: req.ExpirationTime, + CreatedAt: time.Now().UTC(), + LastModified: time.Now().UTC(), + } + + db := conn.WithContext(ctx).Create(req) + if db.Error != nil { + return PersonalAccessToken{}, fmt.Errorf("Failed to create token for user %s", req.UserID) + } + + return token, nil +} + +// Scan() and Value() allow having a list of strings as a type for Scopes +func (s *Scopes) Scan(src any) error { + bytes, ok := src.([]byte) + if !ok { + return errors.New("src value cannot cast to []byte") + } + *s = strings.Split(string(bytes), ",") + return nil +} +func (s Scopes) Value() (driver.Value, error) { + if len(s) == 0 { + return "", nil + } + return strings.Join(s, ","), nil +} diff --git a/components/gitpod-db/go/personal_access_token_test.go b/components/gitpod-db/go/personal_access_token_test.go new file mode 100644 index 00000000000000..26006b9430aca9 --- /dev/null +++ b/components/gitpod-db/go/personal_access_token_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package db_test + +import ( + "context" + "testing" + "time" + + db "github.com/gitpod-io/gitpod/components/gitpod-db/go" + "github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func TestPersonalAccessToken_Get(t *testing.T) { + conn := dbtest.ConnectForTests(t) + + token := db.PersonalAccessToken{ + ID: uuid.New(), + UserID: uuid.New(), + Hash: "some-secure-hash", + Name: "some-name", + Description: "some-description", + Scopes: []string{"read", "write"}, + ExpirationTime: time.Now().Add(5), + CreatedAt: time.Now(), + LastModified: time.Now(), + } + + tx := conn.Create(token) + require.NoError(t, tx.Error) + + result, err := db.GetToken(context.Background(), conn, token.ID) + require.NoError(t, err) + require.Equal(t, token.ID, result.ID) +} + +func TestPersonalAccessToken_Create(t *testing.T) { + conn := dbtest.ConnectForTests(t) + + request := db.PersonalAccessToken{ + ID: uuid.New(), + UserID: uuid.New(), + Hash: "another-secure-hash", + Name: "another-name", + Description: "another-description", + Scopes: []string{"read", "write"}, + ExpirationTime: time.Now().Add(5), + CreatedAt: time.Now(), + LastModified: time.Now(), + } + + result, err := db.CreateToken(context.Background(), conn, request) + require.NoError(t, err) + + require.Equal(t, request.ID, result.ID) +}