diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index e0bfe749a2..f98297285f 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -74,7 +74,7 @@ import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from import { collect, CollectedOperation, legacyCollect } from './usage'; import { generateUnique, getServiceHost, pollForEmailVerificationLink } from './utils'; -function createConnectionPool() { +function getPGConnectionString() { const pg = { user: ensureEnv('POSTGRES_USER'), password: ensureEnv('POSTGRES_PASSWORD'), @@ -83,8 +83,12 @@ function createConnectionPool() { db: ensureEnv('POSTGRES_DB'), }; + return `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`; +} + +function createConnectionPool() { return createPostgresDatabasePool({ - connectionParameters: `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`, + connectionParameters: getPGConnectionString(), }); } @@ -141,6 +145,7 @@ export function initSeed() { return { pollForEmailVerificationLink, + getPGConnectionString, async purgeOIDCDomains() { const pool = await getPool(); await pool.query(psql` diff --git a/integration-tests/tests/api/tokens.spec.ts b/integration-tests/tests/api/tokens.spec.ts index ff646b23a4..873b31bc3c 100644 --- a/integration-tests/tests/api/tokens.spec.ts +++ b/integration-tests/tests/api/tokens.spec.ts @@ -1,5 +1,7 @@ import { pollFor, readTokenInfo } from 'testkit/flow'; import { ProjectType } from 'testkit/gql/graphql'; +import { createTokenStorage } from '@hive/storage'; +import { generateToken } from '@hive/tokens'; import { initSeed } from '../../testkit/seed'; test.concurrent('deleting a token should clear the cache', async () => { @@ -144,3 +146,59 @@ test.concurrent( `); }, ); + +test.concurrent( + 'regression: reading existing token with "last_used_at" from pg database (and not redis cache) does not raise an exception', + async ({ expect }) => { + const seed = initSeed(); + const { createOrg } = await seed.createOwner(); + const { createProject, organization } = await createOrg(); + const { project, target } = await createProject(); + + const tokenStorage = await createTokenStorage(seed.getPGConnectionString(), 1); + + try { + const token = generateToken(); + + // create new token so it does not yet exist in redis cache + const record = await tokenStorage.createToken({ + name: 'foo', + organization: organization.id, + project: project.id, + target: target.id, + scopes: [], + token: token.hash, + tokenAlias: token.alias, + }); + + // touch the token so it has a date + await tokenStorage.touchTokens({ tokens: [{ token: record.token, date: new Date() }] }); + const result = await readTokenInfo(token.secret).then(res => res.expectNoGraphQLErrors()); + expect(result.tokenInfo).toMatchInlineSnapshot(` + { + __typename: TokenInfo, + hasOrganizationDelete: false, + hasOrganizationIntegrations: false, + hasOrganizationMembers: false, + hasOrganizationRead: false, + hasOrganizationSettings: false, + hasProjectAlerts: false, + hasProjectDelete: false, + hasProjectOperationsStoreRead: false, + hasProjectOperationsStoreWrite: false, + hasProjectRead: false, + hasProjectSettings: false, + hasTargetDelete: false, + hasTargetRead: false, + hasTargetRegistryRead: false, + hasTargetRegistryWrite: false, + hasTargetSettings: false, + hasTargetTokensRead: false, + hasTargetTokensWrite: false, + } + `); + } finally { + await tokenStorage.destroy(); + } + }, +); diff --git a/packages/services/storage/src/tokens.ts b/packages/services/storage/src/tokens.ts index b191b21778..9440fa5820 100644 --- a/packages/services/storage/src/tokens.ts +++ b/packages/services/storage/src/tokens.ts @@ -21,7 +21,7 @@ const tokenFields = psql` , "token_alias" AS "tokenAlias" , "name" , to_json("created_at") AS "date" - , "last_used_at" AS "lastUsedAt" + , to_json("last_used_at") AS "lastUsedAt" , "organization_id" AS "organization" , "project_id" AS "project" , "target_id" AS "target" @@ -75,7 +75,7 @@ export async function createTokenStorage( LIMIT 1 `, ) - .then(TokenModel.parse); + .then(TokenModel.nullable().parse); }, async createToken({ token, diff --git a/packages/services/tokens/package.json b/packages/services/tokens/package.json index 6a6a61f253..8e1c658824 100644 --- a/packages/services/tokens/package.json +++ b/packages/services/tokens/package.json @@ -3,6 +3,9 @@ "type": "module", "license": "MIT", "private": true, + "exports": { + ".": "./src/api.ts" + }, "scripts": { "build": "tsx ../../../scripts/runify.ts", "dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/dev.ts", diff --git a/packages/services/tokens/src/api.ts b/packages/services/tokens/src/api.ts index ee8a727bf1..dea440c706 100644 --- a/packages/services/tokens/src/api.ts +++ b/packages/services/tokens/src/api.ts @@ -24,7 +24,7 @@ function hashToken(token: string) { return createHash('sha256').update(token).digest('hex'); } -function generateToken() { +export function generateToken() { const token = createHash('md5') .update(String(Math.random())) .update(String(Date.now())) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b0968e84d..2498e307f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13266,10 +13266,6 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -34813,10 +34809,6 @@ snapshots: globals@16.5.0: {} - globalthis@1.0.3: - dependencies: - define-properties: 1.2.1 - globalthis@1.0.4: dependencies: define-properties: 1.2.1