diff --git a/api/v1/tokens.go b/api/v1/tokens.go index dcd0e99..d774961 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "errors" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" @@ -37,23 +38,32 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken if claims.UserID == "" { return nil, status.Error(codes.Aborted, "Context is invalid") } + + if in.TokenPermissions == nil { + return nil, status.Error(codes.InvalidArgument, "Permissions must be set") + } + permissions := map[string][]string{} + for service, methods := range in.TokenPermissions.Permissions { + permissions[service] = methods.GetMethods() + } + tokenData := &controllers.TokenData{ Name: in.TokenMetadata.GetName(), UserID: claims.UserID, - Scopes: &controllers.Scopes{}, + Scopes: permissions, } token, err := srv.tokenCtrl.Create(ctx, tokenData) if err != nil { - return nil, err + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't create a token") } return &tokens.CreateTokenResponse{ - TokenUuid: &tokens.TokenUUID{}, - TokenMetadata: &tokens.TokenMetadata{}, - TokenPermissions: &tokens.TokenPermissions{}, - TokenValue: &tokens.TokenValue{Token: token}, - }, status.Error(codes.Unimplemented, "Method is not implemented") + TokenValue: &tokens.TokenValue{Token: token}, + }, nil } // ForceTokenExpiration implements [v1.TokensServiceServer]. @@ -86,8 +96,8 @@ func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerSt data := srv.tokenCtrl.ListPermissions(stream.Context()) for key, data := range data { result := &tokens.ListPermissionsResponse{ - Permissions: &tokens.Permissions{ - AvailabiePermissions: map[string]*tokens.MethodList{ + Permissions: &tokens.TokenPermissions{ + Permissions: map[string]*tokens.MethodList{ key: {Methods: data}, }, }, diff --git a/go.mod b/go.mod index 4da5701..c64afdb 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 606e00d..f986d32 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a h1:F21MJw0xsiZf3cj4D+n8JPqkX38XlY+xFju2gQkC9eA= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f h1:zNKrOmQPnn+TPV/Zd6vMTYLb2GySSEyt2VawvjP7wb4= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index fe8252d..2548c6e 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -3,6 +3,7 @@ package controllers import ( "context" "database/sql" + "encoding/json" "errors" "regexp" "time" @@ -37,7 +38,7 @@ type TokenData struct { LastUserAt time.Time RevokedAt time.Time ExpiredAt time.Time - Scopes *Scopes + Scopes map[string][]string } type Scopes struct{} @@ -74,27 +75,97 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin return "", ErrServerError } - query := "INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6)" - if _, err := ctrl.DB.Query(query, id, tokenHash, "dummy", time.Now(), data.ExpiredAt); err != nil { + scopesJson, err := json.Marshal(data.Scopes) + if err != nil { + log.Error(err, "Couldn't marshal permissions into json") + return "", ErrServerError + } + + query := ` + INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, generated_at, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)` + + if _, err := ctrl.DB.Query( + query, + id, + tokenHash, + data.UserID, + scopesJson, + time.Now(), + time.Now(), + data.ExpiredAt, + ); err != nil { log.Error(err, "Couldn't insert a token in the database") return "", ErrServerError } - return "", nil + return tokenValue, nil } // Update token name or permissions, other changes are ignored by this method -func (ctrl *TokenController) Update(ctx context.Context) (string, error) { - return "", nil +func (ctrl *TokenController) Update(ctx context.Context, data *TokenData) error { + log := logger.FromContext(ctx).WithValues("uuid", data.UUID) + log.V(2).Info("Updating a token") + + scopesJson, err := json.Marshal(data.Scopes) + if err != nil { + log.Error(err, "Couldn't marshal permissions into json") + return ErrServerError + } + + query := "UPDATE tokens SET name = $1, scopes = $2 WHERE uuid = $3;" + if _, err := ctrl.DB.Query(query, data.Name, scopesJson, data.UUID); err != nil { + log.Error(err, "Couldn't update a token in the database") + return ErrServerError + } + + return nil } // ForceExpiration of a token, so it can no longer be used -func (ctrl *TokenController) ForceExpiration(ctx context.Context) error { +func (ctrl *TokenController) ForceExpiration(ctx context.Context, id string) error { + log := logger.FromContext(ctx).WithValues("uuid", id) + log.V(2).Info("Forcing a token expiration") + + query := "UPDATE tokens SET revoked_at = $1 WHERE uuid = $2;" + if _, err := ctrl.DB.Query(query, time.Now(), id); err != nil { + log.Error(err, "Couldn't update a token in the database") + return ErrServerError + } + return nil } // Regenerate a token and get a new value -func (ctrl *TokenController) Regenerate(ctx context.Context) (string, error) { +func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string, error) { + log := logger.FromContext(ctx).WithValues("uuid", id) + log.V(2).Info("Regenerating a token") + + tokenValue, err := token.GenerateToken() + if err != nil { + log.Error(err, "Couldn't create a token") + return "", ErrServerError + } + + tokenHash, err := hash.HashPassword(tokenValue, int(ctrl.HashCost)) + if err != nil { + log.Error(err, "Couldn't calculate token hash") + return "", ErrServerError + } + + query := ` + UPDATE tokens + SET + token_hash = $1, + generated_at = $2, + expires_at = NOW() + (expires_at - generated_at), + WHERE uuid = $3;` + + if _, err := ctrl.DB.Query(query, tokenHash, time.Now(), data.UUID); err != nil { + log.Error(err, "Couldn't insert a token in the database") + return "", ErrServerError + } + return "", nil } @@ -104,8 +175,22 @@ func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { } // List all available token -func (ctrl *TokenController) List(ctx context.Context) error { - return nil +func (ctrl *TokenController) List(ctx context.Context, userID string) error { + query := ` + SELECT id, name, generated_at, expires_at + FROM tokens + WHERE user_id = $1` + err := ctrl.DB.QueryRowContext(ctx, query, userID).Scan( + &t.ID, + &t.UserID, + &t.Name, + &scopes, + &t.GeneratedAt, + &t.ExpiresAt, + ) + if err != nil { + return nil, err + } } // Lis all available permissions diff --git a/migrations/20260510174348_tokens_init.up.sql b/migrations/20260510174348_tokens_init.up.sql index aeeb408..eb74e73 100644 --- a/migrations/20260510174348_tokens_init.up.sql +++ b/migrations/20260510174348_tokens_init.up.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS tokens ( user_id UUID NOT NULL, scopes JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + generated_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_used_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, expires_at TIMESTAMPTZ diff --git a/migrations/20260510175121_accounts_timestamptz.down.sql b/migrations/20260510175121_accounts_timestamptz.down.sql deleted file mode 100644 index 56a57eb..0000000 --- a/migrations/20260510175121_accounts_timestamptz.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE accounts -ALTER COLUMN created_at TYPE TIMESTAMP -USING created_at AT TIME ZONE 'UTC'; diff --git a/migrations/20260510175121_accounts_timestamptz.up.sql b/migrations/20260510175121_accounts_timestamptz.up.sql deleted file mode 100644 index 3862462..0000000 --- a/migrations/20260510175121_accounts_timestamptz.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE accounts -ALTER COLUMN created_at TYPE TIMESTAMPTZ -USING created_at AT TIME ZONE 'UTC';