Path: blob/main/components/gitpod-db/go/personal_access_token.go
2498 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package db56import (7"context"8"database/sql/driver"9"errors"10"fmt"11"strings"12"time"1314"github.com/google/uuid"15"gorm.io/gorm"16)1718type PersonalAccessToken struct {19ID uuid.UUID `gorm:"primary_key;column:id;type:varchar;size:255;" json:"id"`20UserID uuid.UUID `gorm:"column:userId;type:varchar;size:255;" json:"userId"`21Hash string `gorm:"column:hash;type:varchar;size:255;" json:"hash"`22Name string `gorm:"column:name;type:varchar;size:255;" json:"name"`23Scopes Scopes `gorm:"column:scopes;type:text;size:65535;" json:"scopes"`24ExpirationTime time.Time `gorm:"column:expirationTime;type:timestamp;" json:"expirationTime"`25CreatedAt time.Time `gorm:"column:createdAt;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"createdAt"`26LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"`2728// deleted is reserved for use by periodic deleter.29_ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"`30}3132// TableName sets the insert table name for this struct type33func (d *PersonalAccessToken) TableName() string {34return "d_b_personal_access_token"35}3637func GetPersonalAccessTokenForUser(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID) (PersonalAccessToken, error) {38var token PersonalAccessToken3940if tokenID == uuid.Nil {41return PersonalAccessToken{}, fmt.Errorf("Token ID is a required argument to get personal access token for user")42}4344if userID == uuid.Nil {45return PersonalAccessToken{}, fmt.Errorf("User ID is a required argument to get personal access token for user")46}4748tx := conn.49WithContext(ctx).50Where("id = ?", tokenID).51Where("userId = ?", userID).52Where("deleted = ?", 0).53First(&token)54if tx.Error != nil {55if errors.Is(tx.Error, gorm.ErrRecordNotFound) {56return PersonalAccessToken{}, fmt.Errorf("Token with ID %s does not exist: %w", tokenID, ErrorNotFound)57}58return PersonalAccessToken{}, fmt.Errorf("Failed to retrieve token: %v", tx.Error)59}6061return token, nil62}6364func CreatePersonalAccessToken(ctx context.Context, conn *gorm.DB, req PersonalAccessToken) (PersonalAccessToken, error) {65if req.UserID == uuid.Nil {66return PersonalAccessToken{}, fmt.Errorf("Invalid or empty userID")67}68if req.Hash == "" {69return PersonalAccessToken{}, fmt.Errorf("Token hash required")70}71if req.Name == "" {72return PersonalAccessToken{}, fmt.Errorf("Token name required")73}74if req.ExpirationTime.IsZero() {75return PersonalAccessToken{}, fmt.Errorf("Expiration time required")76}7778now := time.Now().UTC()79token := PersonalAccessToken{80ID: req.ID,81UserID: req.UserID,82Hash: req.Hash,83Name: req.Name,84Scopes: req.Scopes,85ExpirationTime: req.ExpirationTime,86CreatedAt: now,87LastModified: now,88}8990tx := conn.WithContext(ctx).Create(req)91if tx.Error != nil {92return PersonalAccessToken{}, fmt.Errorf("Failed to create personal access token for user %s", req.UserID)93}9495return token, nil96}9798func UpdatePersonalAccessTokenHash(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID, hash string, expirationTime time.Time) (PersonalAccessToken, error) {99if tokenID == uuid.Nil {100return PersonalAccessToken{}, fmt.Errorf("Invalid or empty tokenID")101}102if userID == uuid.Nil {103return PersonalAccessToken{}, fmt.Errorf("Invalid or empty userID")104}105if hash == "" {106return PersonalAccessToken{}, fmt.Errorf("Token hash required")107}108if expirationTime.IsZero() {109return PersonalAccessToken{}, fmt.Errorf("Expiration time required")110}111112db := conn.WithContext(ctx)113114err := db.115Where("id = ?", tokenID).116Where("userId = ?", userID).117Where("deleted = ?", 0).118Select("hash", "expirationTime").Updates(PersonalAccessToken{Hash: hash, ExpirationTime: expirationTime}).119Error120if err != nil {121if errors.Is(db.Error, gorm.ErrRecordNotFound) {122return PersonalAccessToken{}, fmt.Errorf("Token with ID %s does not exist: %w", tokenID, ErrorNotFound)123}124return PersonalAccessToken{}, fmt.Errorf("Failed to update token: %v", db.Error)125}126127return GetPersonalAccessTokenForUser(ctx, conn, tokenID, userID)128}129130func DeletePersonalAccessTokenForUser(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID) (int64, error) {131if tokenID == uuid.Nil {132return 0, fmt.Errorf("Invalid or empty tokenID")133}134135if userID == uuid.Nil {136return 0, fmt.Errorf("Invalid or empty userID")137}138139db := conn.WithContext(ctx)140141db = db.142Table((&PersonalAccessToken{}).TableName()).143Where("id = ?", tokenID).144Where("userId = ?", userID).145Where("deleted = ?", 0).146Update("deleted", 1)147if db.Error != nil {148return 0, fmt.Errorf("failed to delete token (ID: %s): %v", tokenID.String(), db.Error)149}150151if db.RowsAffected == 0 {152return 0, fmt.Errorf("token (ID: %s) for user (ID: %s) does not exist: %w", tokenID, userID, ErrorNotFound)153}154155return db.RowsAffected, nil156}157158func ListPersonalAccessTokensForUser(ctx context.Context, conn *gorm.DB, userID uuid.UUID, pagination Pagination) (*PaginatedResult[PersonalAccessToken], error) {159if userID == uuid.Nil {160return nil, fmt.Errorf("user ID is a required argument to list personal access tokens for user, got nil")161}162163var results []PersonalAccessToken164165tx := conn.166WithContext(ctx).167Table((&PersonalAccessToken{}).TableName()).168Where("userId = ?", userID).169Where("deleted = ?", 0).170Order("createdAt").171Scopes(Paginate(pagination)).172Find(&results)173if tx.Error != nil {174return nil, fmt.Errorf("failed to list personal access tokens for user %s: %w", userID.String(), tx.Error)175}176177var count int64178tx = conn.179WithContext(ctx).180Table((&PersonalAccessToken{}).TableName()).181Where("userId = ?", userID).182Where("deleted = ?", 0).183Count(&count)184if tx.Error != nil {185return nil, fmt.Errorf("failed to count total number of personal access tokens for user %s: %w", userID.String(), tx.Error)186}187188return &PaginatedResult[PersonalAccessToken]{189Results: results,190Total: count,191}, nil192}193194type UpdatePersonalAccessTokenOpts struct {195TokenID uuid.UUID196UserID uuid.UUID197Name *string198Scopes *Scopes199}200201func UpdatePersonalAccessTokenForUser(ctx context.Context, conn *gorm.DB, opts UpdatePersonalAccessTokenOpts) (PersonalAccessToken, error) {202if opts.TokenID == uuid.Nil {203return PersonalAccessToken{}, errors.New("Token ID is required to udpate personal access token for user")204}205if opts.UserID == uuid.Nil {206return PersonalAccessToken{}, errors.New("User ID is required to udpate personal access token for user")207}208209var cols []string210update := PersonalAccessToken{}211if opts.Name != nil {212cols = append(cols, "name")213update.Name = *opts.Name214}215216if opts.Scopes != nil {217cols = append(cols, "scopes")218update.Scopes = *opts.Scopes219}220221if len(cols) == 0 {222return GetPersonalAccessTokenForUser(ctx, conn, opts.TokenID, opts.UserID)223}224225tx := conn.226WithContext(ctx).227Table((&PersonalAccessToken{}).TableName()).228Where("id = ?", opts.TokenID).229Where("userId = ?", opts.UserID).230Where("deleted = ?", 0).231Select(cols).232Updates(update)233if tx.Error != nil {234return PersonalAccessToken{}, fmt.Errorf("failed to update personal access token: %w", tx.Error)235}236237return GetPersonalAccessTokenForUser(ctx, conn, opts.TokenID, opts.UserID)238}239240type Scopes []string241242// Scan() and Value() allow having a list of strings as a type for Scopes243func (s *Scopes) Scan(src any) error {244bytes, ok := src.([]byte)245if !ok {246return errors.New("src value cannot cast to []byte")247}248249if len(bytes) == 0 {250*s = nil251return nil252}253254*s = strings.Split(string(bytes), ",")255return nil256}257258func (s Scopes) Value() (driver.Value, error) {259if len(s) == 0 {260return "", nil261}262return strings.Join(s, ","), nil263}264265266