Skip to content

Comments

feat: CreateCurrentUserPersonalToken RPC implementation#1401

Open
AmanGIT07 wants to merge 4 commits intomainfrom
feat/create-pat-rpc
Open

feat: CreateCurrentUserPersonalToken RPC implementation#1401
AmanGIT07 wants to merge 4 commits intomainfrom
feat/create-pat-rpc

Conversation

@AmanGIT07
Copy link
Contributor

@AmanGIT07 AmanGIT07 commented Feb 20, 2026

Title: feat: add CreateCurrentUserPersonalToken RPC

Summary

  • Implements the CreateCurrentUserPersonalToken RPC — the first endpoint for Personal Access Tokens (PATs), allowing users to create org-scoped tokens with configurable prefix, expiry, and metadata.
  • Adds full vertical slice: proto codegen, domain model, config, Postgres repository + migration, service layer with token generation (SHA-256 hashed, base64url-encoded), ConnectRPC handler, and authorization interceptor
    wiring.
  • Includes service-level audit logging
  • Adds SpiceDB constants in preparation for future policy integration.

@vercel
Copy link

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Feb 21, 2026 3:25pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Create and manage personal access tokens (PATs) via API endpoint and OpenAPI docs.
    • Persistent storage for PATs with uniqueness per user/org and soft-revoke support.
    • Configurable token policies: enable/disable, prefix, max tokens per user/org, and max expiry.
    • Audit logging for token creation events.
  • Bug Fixes / Validation

    • Request validation for token creation (expiry, formats) with clear error mappings.

Walkthrough

Adds Personal Access Token (PAT) functionality: domain model, service, repository, DB migrations, API RPC and handler, protobuf/schema updates, server wiring and config, authorization rule, mocks, extensive tests, and generated validation code. Also updates Makefile Proton commit.

Changes

Cohort / File(s) Summary
Core PAT domain & service
core/userpat/userpat.go, core/userpat/config.go, core/userpat/errors.go, core/userpat/service.go
Introduces PersonalAccessToken model, Repository interface, Config with expiry/limits, error variables, and Service implementing token creation, validation, audit logging, and token generation.
Core PAT tests & mocks
core/userpat/service_test.go, core/userpat/mocks/*
Adds comprehensive service tests and autogenerated testify mocks for Repository, AuditRecordRepository, and OrganizationService.
API wiring & handler
cmd/serve.go, internal/api/api.go, internal/api/v1beta1connect/v1beta1connect.go, internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/user_pat.go, internal/api/v1beta1connect/user_pat_test.go, internal/api/v1beta1connect/mocks/user_pat_service.go
Wires UserPATService into server deps, extends ConnectHandler constructor to accept PAT config, adds UserPATService interface, implements CreateCurrentUserPersonalToken RPC handler, tests, and mocks; maps service errors to Connect errors.
Postgres repository & model
internal/store/postgres/userpat.go, internal/store/postgres/userpat_repository.go, internal/store/postgres/userpat_repository_test.go, internal/store/postgres/postgres.go
Adds internal UserToken model, transform logic, Postgres-backed UserPATRepository with Create and CountActive, tests, and TABLE_USER_TOKENS constant.
DB migrations
internal/store/postgres/migrations/20260218100000_create_user_tokens.up.sql, ...down.sql
Creates user_tokens table, indexes, unique partial constraint for active tokens, trigger/function for updated_at, and corresponding down migration.
Server config & authz
pkg/server/config.go, pkg/server/connect_interceptors/authorization.go, internal/bootstrap/schema/schema.go
Adds PAT config to server config, authorization rule for CreateCurrentUserPersonalToken, and schema constants (PATPrincipal, PATGrantRelationName).
Protobuf, validation & docs
proto/v1beta1/frontier.pb.validate.go, proto/v1beta1/models.pb.validate.go, proto/v1beta1/frontierv1beta1connect/frontier.connect.go, proto/apidocs.swagger.yaml
Adds extensive generated validation code, PersonalAccessToken proto definition, CreateCurrentUserPersonalToken RPC (client/server/routing), and swagger doc updates.
Server startup
pkg/server/server.go
Updates Connect handler construction call to pass new PAT config parameter.
Build config
Makefile
Updates PROTON_COMMIT hash used for protobuf generation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • rohilsurana
  • rsbh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/bootstrap/schema/schema.go (1)

255-260: ⚠️ Potential issue | 🟡 Minor

Include PATPrincipal in IsSystemNamespace() for consistency with other principals.

All other principals—UserPrincipal, ServiceUserPrincipal, SuperUserPrincipal, GroupPrincipal—are included in the check, but PATPrincipal (defined at line 75) is omitted. Since PATPrincipal is recognized as a valid namespace alias in ParseNamespaceAliasIfRequired(), it should be treated the same way as other principals to maintain consistent behavior.

🧹 Nitpick comments (9)
pkg/auditrecord/consts.go (1)

57-58: Consider adding a PATDeletedEvent / PATRevokedEvent constant for completeness.

Every other audited resource with a lifecycle (service users, sessions, billing entities) has at minimum a deletion/revocation event alongside the creation event. If PAT revocation or deletion is planned, pre-declaring the constant here keeps the audit vocabulary consistent and avoids a follow-up PR solely to add "pat.deleted" or "pat.revoked".

♻️ Proposed addition
 	// PAT Events
 	PATCreatedEvent Event = "pat.created"
+	PATDeletedEvent Event = "pat.deleted"
core/userpat/errors.go (1)

1-14: LGTM — clean, idiomatic sentinel error set.

The var block with errors.New is the canonical Go pattern for package-level sentinel errors. All eight values are exported, clearly named with the Err prefix, and carry descriptive messages that unambiguously attribute them to the PAT domain. The distinction between ErrExpiryInPast (creation-time validation) and ErrExpiryExceeded (lifetime cap) is a nice touch.

One optional consideration: adding a short package prefix such as "userpat: personal access token not found" to each message can improve discoverability when these errors are wrapped and appear deep in a multi-layer error chain or log stream. Since "personal access token" already appears in every string, the attribution is already clear—this is purely a style preference.

cmd/serve.go (1)

391-436: userPATRepo declaration is separated from userPATService creation by ~45 unrelated lines.

Every other repo/service pair in this function is declared back-to-back (e.g., prospectRepository/prospectService at lines 388-389). Moving userPATRepo immediately before userPATService would keep the locality consistent.

♻️ Suggested repositioning
-	userPATRepo := postgres.NewUserPATRepository(dbc)
-
 	svUserRepo := postgres.NewServiceUserRepository(dbc)
 	// ... (lines 393-435 unchanged) ...
 
 	organizationService := organization.NewService(...)
+
+	userPATRepo := postgres.NewUserPATRepository(dbc)
 	userPATService := userpat.NewService(userPATRepo, cfg.App.PAT, organizationService, auditRecordRepository)
core/userpat/config.go (1)

10-10: DefaultTokenLifetime has no DefaultExpiry() helper to match MaxExpiry().

Any code that needs the parsed default lifetime must replicate the time.ParseDuration + fallback logic inline, risking divergence. Adding a symmetric helper ensures consistent parsing behaviour.

♻️ Proposed addition
+func (c Config) DefaultExpiry() time.Duration {
+	d, err := time.ParseDuration(c.DefaultTokenLifetime)
+	if err != nil {
+		return 90 * 24 * time.Hour // matches the "2160h" default
+	}
+	return d
+}
internal/api/v1beta1connect/user_pat.go (1)

84-89: Silently swallowed metadata conversion error may hide data loss.

If ToStructPB() fails (e.g., due to unsupported value types in metadata), the error is ignored and metadata is silently omitted from the response. Consider logging the error so operators can diagnose missing metadata.

🔧 Proposed fix
 	if pat.Metadata != nil {
 		metaPB, err := pat.Metadata.ToStructPB()
 		if err == nil {
 			pbPAT.Metadata = metaPB
+		} else {
+			// Log metadata conversion failure so it's diagnosable
+			zap.L().Warn("failed to convert PAT metadata to protobuf", zap.String("pat_id", pat.ID), zap.Error(err))
 		}
 	}
core/userpat/service.go (2)

88-88: Roles and ProjectIDs are silently discarded

Both fields flow through CreateRequest, are passed into the audit metadata, but the TODO means no policies are created. Any caller relying on the returned token having the scoped access they requested will get an over-privileged (or under-privileged) token with no error surfaced.

Would you like me to open a tracking issue or scaffold the policy-creation step once the role/project model is available?


107-108: maps.Copy silently overwrites "user_id" if targetMetadata contains that key

maps.Copy copies all key/value pairs from src into dst; when a key in src is already present in dst, the value in dst is overwritten. If any future caller of createAuditRecord passes "user_id" inside targetMetadata, the explicitly set pat.UserID binding will be silently replaced. Build the merged map in the other order to give the explicit user_id precedence:

♻️ Proposed fix
-	metadata := map[string]any{"user_id": pat.UserID}
-	maps.Copy(metadata, targetMetadata)
+	metadata := maps.Clone(targetMetadata)
+	metadata["user_id"] = pat.UserID  // always authoritative
core/userpat/service_test.go (2)

272-301: "should hash the full token string" test case verifies nothing about the stored hash

The assertion — that sha256(tokenValue) produces a 64-character hex string — is trivially true for any non-empty input and carries zero coverage value. It does not capture SecretHash from the mock call or compare it to the token. TestService_Create_HashVerification (lines 428-458) already does the correct end-to-end hash verification. Consider removing this table case or replacing it with a comment referencing the standalone test.


353-372: Table case "should generate unique tokens on each call" makes only one Create call

Within the table loop body (lines 374-394) each case calls Create exactly once, so this case cannot demonstrate uniqueness. It reads as an ownership claim that the table runner doesn't fulfil. TestService_Create_UniqueTokens (lines 397-426) already covers uniqueness correctly. Rename this case to something that reflects its actual intent (e.g., "should succeed when count is 0") or remove it as a duplicate.

Comment on lines +706 to +709
frontierv1beta1connect.FrontierServiceCreateCurrentUserPersonalTokenProcedure: func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
pbreq := req.(*connect.Request[frontierv1beta1.CreateCurrentUserPersonalTokenRequest])
return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.Msg.GetOrgId()}, schema.GetPermission, req)
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all PAT-related procedure constants and check authorization maps

echo "=== PAT-related procedure constants in codebase ==="
rg -n "PersonalToken" --type go

echo ""
echo "=== PAT procedures in authorization.go ==="
rg -n "PersonalToken" pkg/server/connect_interceptors/authorization.go

Repository: raystack/frontier

Length of output: 20840


🏁 Script executed:

#!/bin/bash
# Read the authorization map section to see exact permission for CreateCurrentUserPersonalToken

echo "=== Authorization entry for CreateCurrentUserPersonalToken (lines 700-715) ==="
sed -n '700,715p' pkg/server/connect_interceptors/authorization.go

echo ""
echo "=== Other Create* endpoints and their permissions ==="
rg -n "^[[:space:]]*frontierv1beta1connect\.FrontierServiceCreate" pkg/server/connect_interceptors/authorization.go -A 2

echo ""
echo "=== Schema permission constants ==="
rg -n "^[[:space:]]*(Get|Update|Create|Manage)Permission\s*=" --type go | head -30

Repository: raystack/frontier

Length of output: 2490


🏁 Script executed:

#!/bin/bash
# Search for other Create* endpoints to verify the pattern

echo "=== All Create* endpoints in authorization.go with their permissions ==="
rg -n "FrontierServiceCreate|AdminServiceCreate" pkg/server/connect_interceptors/authorization.go -A 2 | head -80

Repository: raystack/frontier

Length of output: 891


🏁 Script executed:

#!/bin/bash
# Search more broadly for all Create* procedures to see the full pattern

echo "=== All procedures matching Create pattern ==="
rg -n "case.*ServiceCreate" pkg/server/connect_interceptors/authorization.go -B 1 | head -100

echo ""
echo "=== Broader search: all FrontierServiceCreate procedures with full context ==="
rg -n "FrontierServiceCreate\w+Procedure:" pkg/server/connect_interceptors/authorization.go -A 2 | head -120

Repository: raystack/frontier

Length of output: 945


🏁 Script executed:

#!/bin/bash
# Get a comprehensive view of all entries in the authorizationValidationMap

echo "=== Full authorizationValidationMap from authorization.go ==="
# Extract the entire map - assuming it's bounded by "var authorizationValidationMap" and "}"
awk '/var authorizationValidationMap/,/^}/' pkg/server/connect_interceptors/authorization.go | grep -E "(Procedure:|Permission|func\()" | head -100

Repository: raystack/frontier

Length of output: 15806


🏁 Script executed:

#!/bin/bash
# Verify the actual permission values and understand the Create* endpoint patterns

echo "=== Schema permission constants definitions ==="
cat internal/bootstrap/schema/schema.go | grep -A 20 "GetPermission\|CreatePermission\|UpdatePermission\|ManagePermission" | head -30

echo ""
echo "=== Summary of Create* endpoints and their permissions ==="
rg "Create\w+Procedure.*Permission" pkg/server/connect_interceptors/authorization.go -o | sort | uniq

Repository: raystack/frontier

Length of output: 1254


🏁 Script executed:

#!/bin/bash
# Get a cleaner view of all Create* procedures and their permissions

echo "=== All Create* procedures and their permissions ==="
rg -n "Create\w+Procedure.*func\(ctx" pkg/server/connect_interceptors/authorization.go -A 2 | grep -E "(Procedure:|Permission)" | paste - - | head -40

Repository: raystack/frontier

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Get all Create* procedures with their permissions

echo "=== All Create* procedures and their permissions ==="
rg -n "^\s+frontierv1beta1connect\.FrontierServiceCreate" pkg/server/connect_interceptors/authorization.go -A 2 | grep -v "^--$"

Repository: raystack/frontier

Length of output: 868


🏁 Script executed:

#!/bin/bash
# Verify that CreateProject and CreateOrganizationRole exist with their permissions

echo "=== Searching for CreateProject and CreateOrganizationRole ==="
rg -n "CreateProject|CreateRole" pkg/server/connect_interceptors/authorization.go | head -20

Repository: raystack/frontier

Length of output: 1036


🏁 Script executed:

#!/bin/bash
# Get the full context for CreateProject, CreateRole, and other Create* endpoints

echo "=== CreateProject permission ==="
sed -n '464,467p' pkg/server/connect_interceptors/authorization.go

echo ""
echo "=== CreateRole permission ==="
sed -n '1039,1042p' pkg/server/connect_interceptors/authorization.go

echo ""
echo "=== CreateGroup permission ==="
sed -n '555,558p' pkg/server/connect_interceptors/authorization.go

echo ""
echo "=== CreateOrganizationInvitation permission ==="
sed -n '494,497p' pkg/server/connect_interceptors/authorization.go

echo ""
echo "=== CreateServiceUser permission ==="
sed -n '410,413p' pkg/server/connect_interceptors/authorization.go

Repository: raystack/frontier

Length of output: 2144


🏁 Script executed:

#!/bin/bash
# Use rg to find and display the exact lines with context

echo "=== CreateProject ==="
rg -n "CreateProject.*func\(ctx" pkg/server/connect_interceptors/authorization.go -A 2 | head -5

echo ""
echo "=== CreateGroup ==="
rg -n "CreateGroup.*func\(ctx" pkg/server/connect_interceptors/authorization.go -A 2 | head -5

echo ""
echo "=== CreateOrganizationInvitation ==="
rg -n "CreateOrganizationInvitation.*func\(ctx" pkg/server/connect_interceptors/authorization.go -A 2 | head -5

echo ""
echo "=== CreateServiceUser ==="
rg -n "CreateServiceUser.*func\(ctx" pkg/server/connect_interceptors/authorization.go -A 2 | head -5

Repository: raystack/frontier

Length of output: 2335


GetPermission (read) gates a token-creation write operation — inconsistent with all other Create* endpoints.

Every other Create* endpoint in authorizationValidationMap uses a write-level or specialized-manage permission as the authorization gate:

Endpoint Permission
CreateServiceUser ServiceUserManagePermission
CreateOrganizationInvitation InvitationCreatePermission
CreateGroup GroupCreatePermission
CreateProject ProjectCreatePermission

CreateCurrentUserPersonalToken uses only schema.GetPermission ("get"), meaning any read-only org member can create a PAT scoped to that org. Since PATs carry Roles and ProjectIds fields that can embed sensitive permissions, this allows a low-privileged member to create tokens with elevated role claims — a privilege escalation risk.

Unless the intent is explicitly to allow any org member to self-issue PATs, the permission should be raised — at minimum to schema.UpdatePermission, or ideally a dedicated PAT*ManagePermission constant.

Suggested fix
- return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.Msg.GetOrgId()}, schema.GetPermission, req)
+ return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.Msg.GetOrgId()}, schema.UpdatePermission, req)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
frontierv1beta1connect.FrontierServiceCreateCurrentUserPersonalTokenProcedure: func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
pbreq := req.(*connect.Request[frontierv1beta1.CreateCurrentUserPersonalTokenRequest])
return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.Msg.GetOrgId()}, schema.GetPermission, req)
},
frontierv1beta1connect.FrontierServiceCreateCurrentUserPersonalTokenProcedure: func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
pbreq := req.(*connect.Request[frontierv1beta1.CreateCurrentUserPersonalTokenRequest])
return handler.IsAuthorized(ctx, relation.Object{Namespace: schema.OrganizationNamespace, ID: pbreq.Msg.GetOrgId()}, schema.UpdatePermission, req)
},

@coveralls
Copy link

coveralls commented Feb 20, 2026

Pull Request Test Coverage Report for Build 22259293803

Details

  • 208 of 244 (85.25%) changed or added relevant lines in 10 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.3%) to 38.728%

Changes Missing Coverage Covered Lines Changed/Added Lines %
pkg/server/server.go 0 1 0.0%
internal/api/v1beta1connect/user_pat.go 66 68 97.06%
internal/bootstrap/schema/schema.go 0 2 0.0%
internal/store/postgres/userpat.go 16 18 88.89%
internal/api/v1beta1connect/v1beta1connect.go 0 3 0.0%
pkg/server/connect_interceptors/authorization.go 0 4 0.0%
cmd/serve.go 0 5 0.0%
core/userpat/service.go 75 83 90.36%
internal/store/postgres/userpat_repository.go 45 54 83.33%
Totals Coverage Status
Change from base Build 22214392467: 0.3%
Covered Lines: 16408
Relevant Lines: 42367

💛 - Coveralls

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Comment on lines +354 to +373
{
name: "should generate unique tokens on each call",
req: userpat.CreateRequest{
UserID: "user-1",
OrgID: "org-1",
Title: "my-token",
Roles: []string{"role-1"},
ExpiresAt: time.Now().Add(24 * time.Hour),
},
wantErr: false,
setup: func() *userpat.Service {
repo := mocks.NewRepository(t)
repo.EXPECT().CountActive(mock.Anything, "user-1", "org-1").
Return(int64(0), nil)
repo.EXPECT().Create(mock.Anything, mock.AnythingOfType("userpat.PersonalAccessToken")).
Return(userpat.PersonalAccessToken{ID: "pat-1", OrgID: "org-1"}, nil)
orgSvc, auditRepo := newSuccessMocks(t)
return userpat.NewService(log.NewNoop(), repo, defaultConfig, orgSvc, auditRepo)
},
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"should generate unique tokens on each call" makes only one Create call — test name is misleading

The table case sets up the repo mock with a single CountActive + Create expectation and has no validateFunc, so it just verifies that one call succeeds without errors. Uniqueness across two calls is already tested properly in TestService_Create_UniqueTokens (line 398). This case either duplicates existing "happy-path creation" coverage or the test author intended it to assert uniqueness but forgot the second call.

Consider removing this case (redundant with other happy-path cases) or renaming it to reflect what it actually exercises.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/api/v1beta1connect/user_pat.go (2)

87-91: Log metadata serialisation failure in transformPATToPB instead of silently dropping it.

If ToStructPB() fails, the response silently returns nil metadata. A caller receiving a token with no metadata has no way to distinguish "empty metadata" from "serialisation error". At minimum, log at warn so it surfaces during debugging.

🛠️ Proposed fix
 	if pat.Metadata != nil {
 		metaPB, err := pat.Metadata.ToStructPB()
 		if err == nil {
 			pbPAT.Metadata = metaPB
+		} else {
+			// log and continue: the token is valid, only metadata rendering failed
+			// h.logger.Warn("failed to convert PAT metadata to protobuf", zap.Error(err))
 		}
 	}

32-46: Reuse expiresAt instead of calling .AsTime() twice.

request.Msg.GetExpiresAt().AsTime() is invoked at line 32 and again at line 46 inside the CreateRequest literal. Reuse the already-computed value.

🛠️ Proposed fix
 	expiresAt := request.Msg.GetExpiresAt().AsTime()
 	if !expiresAt.After(time.Now()) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, userpat.ErrExpiryInPast)
 	}
 	if expiresAt.After(time.Now().Add(h.patConfig.MaxExpiry())) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, userpat.ErrExpiryExceeded)
 	}

 	created, tokenValue, err := h.userPATService.Create(ctx, userpat.CreateRequest{
 		...
-		ExpiresAt:  request.Msg.GetExpiresAt().AsTime(),
+		ExpiresAt:  expiresAt,
 		...
 	})

Comment on lines +49 to +64
if err != nil {
errorLogger.LogServiceError(ctx, request, "CreateCurrentUserPersonalToken", err,
zap.String("user_id", principal.User.ID),
zap.String("org_id", request.Msg.GetOrgId()))

switch {
case errors.Is(err, userpat.ErrDisabled):
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
case errors.Is(err, userpat.ErrConflict):
return nil, connect.NewError(connect.CodeAlreadyExists, err)
case errors.Is(err, userpat.ErrLimitExceeded):
return nil, connect.NewError(connect.CodeResourceExhausted, err)
default:
return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move LogServiceError to the default case — expected errors pollute error logs.

LogServiceError fires unconditionally for all errors, so ErrDisabled (feature flag off), ErrConflict (duplicate title), and ErrLimitExceeded (quota reached) — all normal user-facing outcomes — generate error-level log entries. This degrades log signal and can trigger false alerts.

🛠️ Proposed fix
-	errorLogger.LogServiceError(ctx, request, "CreateCurrentUserPersonalToken", err,
-		zap.String("user_id", principal.User.ID),
-		zap.String("org_id", request.Msg.GetOrgId()))
-
 	switch {
 	case errors.Is(err, userpat.ErrDisabled):
 		return nil, connect.NewError(connect.CodeFailedPrecondition, err)
 	case errors.Is(err, userpat.ErrConflict):
 		return nil, connect.NewError(connect.CodeAlreadyExists, err)
 	case errors.Is(err, userpat.ErrLimitExceeded):
 		return nil, connect.NewError(connect.CodeResourceExhausted, err)
 	default:
+		errorLogger.LogServiceError(ctx, request, "CreateCurrentUserPersonalToken", err,
+			zap.String("user_id", principal.User.ID),
+			zap.String("org_id", request.Msg.GetOrgId()))
 		return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError)
 	}

org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
secret_hash TEXT NOT NULL UNIQUE,
metadata JSONB DEFAULT '{}',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add NOT NULL to metadata column.

DEFAULT '{}' only applies when the column is omitted from an INSERT. If the Go driver sends an explicit NULL (which it will for a nil map[string]any), the default is bypassed and the row stores NULL. This breaks any downstream code that relies on metadata always being a valid JSONB object (e.g., transformPATToPB in the handler, which guards on pat.Metadata != nil).

🛠️ Proposed fix
-    metadata JSONB DEFAULT '{}',
+    metadata JSONB NOT NULL DEFAULT '{}',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
metadata JSONB DEFAULT '{}',
metadata JSONB NOT NULL DEFAULT '{}',

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants