mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Change PAT primary key from string to number (#2163)
* Update primary key * Fix tests
This commit is contained in:
		
							parent
							
								
									879e1358ef
								
							
						
					
					
						commit
						c105ca02f1
					
				| @ -6,8 +6,8 @@ import NotFoundError from '../error/notfound-error'; | |||||||
| 
 | 
 | ||||||
| const TABLE = 'personal_access_tokens'; | const TABLE = 'personal_access_tokens'; | ||||||
| 
 | 
 | ||||||
| const PAT_COLUMNS = [ | const PAT_PUBLIC_COLUMNS = [ | ||||||
|     'secret', |     'id', | ||||||
|     'description', |     'description', | ||||||
|     'user_id', |     'user_id', | ||||||
|     'expires_at', |     'expires_at', | ||||||
| @ -20,6 +20,7 @@ const fromRow = (row) => { | |||||||
|         throw new NotFoundError('No PAT found'); |         throw new NotFoundError('No PAT found'); | ||||||
|     } |     } | ||||||
|     return new Pat({ |     return new Pat({ | ||||||
|  |         id: row.id, | ||||||
|         secret: row.secret, |         secret: row.secret, | ||||||
|         userId: row.user_id, |         userId: row.user_id, | ||||||
|         description: row.description, |         description: row.description, | ||||||
| @ -51,8 +52,12 @@ export default class PatStore implements IPatStore { | |||||||
|         return fromRow(row[0]); |         return fromRow(row[0]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async delete(secret: string): Promise<void> { |     async delete(id: number): Promise<void> { | ||||||
|         return this.db(TABLE).where({ secret: secret }).del(); |         return this.db(TABLE).where({ id: id }).del(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async deleteForUser(id: number, userId: number): Promise<void> { | ||||||
|  |         return this.db(TABLE).where({ id: id, user_id: userId }).del(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async deleteAll(): Promise<void> { |     async deleteAll(): Promise<void> { | ||||||
| @ -61,28 +66,28 @@ export default class PatStore implements IPatStore { | |||||||
| 
 | 
 | ||||||
|     destroy(): void {} |     destroy(): void {} | ||||||
| 
 | 
 | ||||||
|     async exists(secret: string): Promise<boolean> { |     async exists(id: number): Promise<boolean> { | ||||||
|         const result = await this.db.raw( |         const result = await this.db.raw( | ||||||
|             `SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`, |             `SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, | ||||||
|             [secret], |             [id], | ||||||
|         ); |         ); | ||||||
|         const { present } = result.rows[0]; |         const { present } = result.rows[0]; | ||||||
|         return present; |         return present; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async get(secret: string): Promise<Pat> { |     async get(id: number): Promise<Pat> { | ||||||
|         const row = await this.db(TABLE).where({ secret }).first(); |         const row = await this.db(TABLE).where({ id }).first(); | ||||||
|         return fromRow(row); |         return fromRow(row); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAll(): Promise<Pat[]> { |     async getAll(): Promise<Pat[]> { | ||||||
|         const groups = await this.db.select(PAT_COLUMNS).from(TABLE); |         const groups = await this.db.select(PAT_PUBLIC_COLUMNS).from(TABLE); | ||||||
|         return groups.map(fromRow); |         return groups.map(fromRow); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAllByUser(userId: number): Promise<Pat[]> { |     async getAllByUser(userId: number): Promise<Pat[]> { | ||||||
|         const groups = await this.db |         const groups = await this.db | ||||||
|             .select(PAT_COLUMNS) |             .select(PAT_PUBLIC_COLUMNS) | ||||||
|             .from(TABLE) |             .from(TABLE) | ||||||
|             .where('user_id', userId); |             .where('user_id', userId); | ||||||
|         return groups.map(fromRow); |         return groups.map(fromRow); | ||||||
|  | |||||||
| @ -4,6 +4,9 @@ export const patSchema = { | |||||||
|     $id: '#/components/schemas/patSchema', |     $id: '#/components/schemas/patSchema', | ||||||
|     type: 'object', |     type: 'object', | ||||||
|     properties: { |     properties: { | ||||||
|  |         id: { | ||||||
|  |             type: 'number', | ||||||
|  |         }, | ||||||
|         secret: { |         secret: { | ||||||
|             type: 'string', |             type: 'string', | ||||||
|         }, |         }, | ||||||
|  | |||||||
| @ -62,7 +62,7 @@ export default class PatController extends Controller { | |||||||
| 
 | 
 | ||||||
|         this.route({ |         this.route({ | ||||||
|             method: 'delete', |             method: 'delete', | ||||||
|             path: '/:secret', |             path: '/:id', | ||||||
|             acceptAnyContentType: true, |             acceptAnyContentType: true, | ||||||
|             handler: this.deletePat, |             handler: this.deletePat, | ||||||
|             permission: NONE, |             permission: NONE, | ||||||
| @ -95,11 +95,11 @@ export default class PatController extends Controller { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async deletePat( |     async deletePat( | ||||||
|         req: IAuthRequest<{ secret: string }>, |         req: IAuthRequest<{ id: number }>, | ||||||
|         res: Response, |         res: Response, | ||||||
|     ): Promise<void> { |     ): Promise<void> { | ||||||
|         const { secret } = req.params; |         const { id } = req.params; | ||||||
|         await this.patService.deletePat(secret); |         await this.patService.deletePat(id, req.user.id); | ||||||
|         res.status(200).end(); |         res.status(200).end(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -37,6 +37,7 @@ export default class PatService { | |||||||
|         pat.userId = user.id; |         pat.userId = user.id; | ||||||
|         const newPat = await this.patStore.create(pat); |         const newPat = await this.patStore.create(pat); | ||||||
| 
 | 
 | ||||||
|  |         pat.secret = '***'; | ||||||
|         await this.eventStore.store({ |         await this.eventStore.store({ | ||||||
|             type: PAT_CREATED, |             type: PAT_CREATED, | ||||||
|             createdBy: user.email || user.username, |             createdBy: user.email || user.username, | ||||||
| @ -50,8 +51,8 @@ export default class PatService { | |||||||
|         return this.patStore.getAllByUser(user.id); |         return this.patStore.getAllByUser(user.id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async deletePat(secret: string): Promise<void> { |     async deletePat(id: number, userId: number): Promise<void> { | ||||||
|         return this.patStore.delete(secret); |         return this.patStore.deleteForUser(id, userId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private generateSecretKey() { |     private generateSecretKey() { | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| export interface IPat { | export interface IPat { | ||||||
|  |     id: number; | ||||||
|     secret: string; |     secret: string; | ||||||
|     description: string; |     description: string; | ||||||
|     userId: number; |     userId: number; | ||||||
| @ -8,6 +9,8 @@ export interface IPat { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class Pat implements IPat { | export default class Pat implements IPat { | ||||||
|  |     id: number; | ||||||
|  | 
 | ||||||
|     secret: string; |     secret: string; | ||||||
| 
 | 
 | ||||||
|     description: string; |     description: string; | ||||||
| @ -21,13 +24,15 @@ export default class Pat implements IPat { | |||||||
|     createdAt: Date; |     createdAt: Date; | ||||||
| 
 | 
 | ||||||
|     constructor({ |     constructor({ | ||||||
|         secret, |         id, | ||||||
|         userId, |         userId, | ||||||
|         expiresAt, |         expiresAt, | ||||||
|         seenAt, |         seenAt, | ||||||
|         createdAt, |         createdAt, | ||||||
|  |         secret, | ||||||
|         description, |         description, | ||||||
|     }: IPat) { |     }: IPat) { | ||||||
|  |         this.id = id; | ||||||
|         this.secret = secret; |         this.secret = secret; | ||||||
|         this.userId = userId; |         this.userId = userId; | ||||||
|         this.expiresAt = expiresAt; |         this.expiresAt = expiresAt; | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| import { Store } from './store'; | import { Store } from './store'; | ||||||
| import { IPat } from '../models/pat'; | import { IPat } from '../models/pat'; | ||||||
| 
 | 
 | ||||||
| export interface IPatStore extends Store<IPat, string> { | export interface IPatStore extends Store<IPat, number> { | ||||||
|     create(group: IPat): Promise<IPat>; |     create(group: IPat): Promise<IPat>; | ||||||
|     getAllByUser(userId: number): Promise<IPat[]>; |     getAllByUser(userId: number): Promise<IPat[]>; | ||||||
|  |     deleteForUser(id: number, userId: number): Promise<void>; | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								src/migrations/20221010114644-pat-auto-increment.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/migrations/20221010114644-pat-auto-increment.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | exports.up = function (db, cb) { | ||||||
|  |     db.runSql( | ||||||
|  |         ` | ||||||
|  |             alter table personal_access_tokens | ||||||
|  |                 drop constraint personal_access_tokens_pkey; | ||||||
|  |             ALTER TABLE personal_access_tokens | ||||||
|  |                 add column id serial primary key; | ||||||
|  |         `,
 | ||||||
|  |         cb, | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | exports.down = function (db, cb) { | ||||||
|  |     db.runSql( | ||||||
|  |         ` | ||||||
|  |             alter table personal_access_tokens | ||||||
|  |                 drop constraint personal_access_tokens_pkey; | ||||||
|  |             alter table personal_access_tokens | ||||||
|  |                 drop column id; | ||||||
|  |             alter table personal_access_tokens | ||||||
|  |                 add primary key (secret); | ||||||
|  | 
 | ||||||
|  |         `,
 | ||||||
|  |         cb, | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -8,6 +8,7 @@ let db: ITestDb; | |||||||
| 
 | 
 | ||||||
| let tomorrow = new Date(); | let tomorrow = new Date(); | ||||||
| let firstSecret; | let firstSecret; | ||||||
|  | let firstId; | ||||||
| tomorrow.setDate(tomorrow.getDate() + 1); | tomorrow.setDate(tomorrow.getDate() + 1); | ||||||
| 
 | 
 | ||||||
| beforeAll(async () => { | beforeAll(async () => { | ||||||
| @ -44,6 +45,7 @@ test('should create a PAT', async () => { | |||||||
|     expect(new Date(body.expiresAt)).toEqual(tomorrow); |     expect(new Date(body.expiresAt)).toEqual(tomorrow); | ||||||
|     expect(body.description).toEqual(description); |     expect(body.description).toEqual(description); | ||||||
|     firstSecret = body.secret; |     firstSecret = body.secret; | ||||||
|  |     firstId = body.id; | ||||||
| 
 | 
 | ||||||
|     const response = await request |     const response = await request | ||||||
|         .get('/api/admin/user/tokens') |         .get('/api/admin/user/tokens') | ||||||
| @ -64,9 +66,9 @@ test('should delete the PAT', async () => { | |||||||
|         .set('Content-Type', 'application/json') |         .set('Content-Type', 'application/json') | ||||||
|         .expect(201); |         .expect(201); | ||||||
| 
 | 
 | ||||||
|     const createdSecret = body.secret; |     const createdId = body.id; | ||||||
| 
 | 
 | ||||||
|     await request.delete(`/api/admin/user/tokens/${createdSecret}`).expect(200); |     await request.delete(`/api/admin/user/tokens/${createdId}`).expect(200); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('should get all PATs', async () => { | test('should get all PATs', async () => { | ||||||
| @ -78,6 +80,36 @@ test('should get all PATs', async () => { | |||||||
|         .expect(200); |         .expect(200); | ||||||
| 
 | 
 | ||||||
|     expect(body.pats).toHaveLength(1); |     expect(body.pats).toHaveLength(1); | ||||||
|  |     expect(body.pats[0].secret).toBeUndefined(); | ||||||
|  |     expect(body.pats[0].id).toBeDefined(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('should not allow deletion of other users PAT', async () => { | ||||||
|  |     const { request } = app; | ||||||
|  | 
 | ||||||
|  |     await app.request | ||||||
|  |         .post(`/auth/demo/login`) | ||||||
|  |         .send({ | ||||||
|  |             email: 'user-second@getunleash.io', | ||||||
|  |         }) | ||||||
|  |         .expect(200); | ||||||
|  | 
 | ||||||
|  |     await request.delete(`/api/admin/user/tokens/${firstId}`).expect(200); | ||||||
|  | 
 | ||||||
|  |     await app.request | ||||||
|  |         .post(`/auth/demo/login`) | ||||||
|  |         .send({ | ||||||
|  |             email: 'user@getunleash.io', | ||||||
|  |         }) | ||||||
|  |         .expect(200); | ||||||
|  | 
 | ||||||
|  |     const { body } = await request | ||||||
|  |         .get('/api/admin/user/tokens') | ||||||
|  |         .expect('Content-Type', /json/) | ||||||
|  |         .expect(200); | ||||||
|  | 
 | ||||||
|  |     expect(body.pats).toHaveLength(1); | ||||||
|  |     expect(body.pats[0].secret).toBeUndefined(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('should get only current user PATs', async () => { | test('should get only current user PATs', async () => { | ||||||
|  | |||||||
| @ -1688,6 +1688,9 @@ exports[`should serve the OpenAPI spec 1`] = ` | |||||||
|             "nullable": true, |             "nullable": true, | ||||||
|             "type": "string", |             "type": "string", | ||||||
|           }, |           }, | ||||||
|  |           "id": { | ||||||
|  |             "type": "number", | ||||||
|  |           }, | ||||||
|           "secret": { |           "secret": { | ||||||
|             "type": "string", |             "type": "string", | ||||||
|           }, |           }, | ||||||
| @ -7051,13 +7054,13 @@ If the provided project does not exist, the list of events will be empty.", | |||||||
|         ], |         ], | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "/api/admin/user/tokens/{secret}": { |     "/api/admin/user/tokens/{id}": { | ||||||
|       "delete": { |       "delete": { | ||||||
|         "operationId": "deletePat", |         "operationId": "deletePat", | ||||||
|         "parameters": [ |         "parameters": [ | ||||||
|           { |           { | ||||||
|             "in": "path", |             "in": "path", | ||||||
|             "name": "secret", |             "name": "id", | ||||||
|             "required": true, |             "required": true, | ||||||
|             "schema": { |             "schema": { | ||||||
|               "type": "string", |               "type": "string", | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								src/test/fixtures/fake-pat-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								src/test/fixtures/fake-pat-store.ts
									
									
									
									
										vendored
									
									
								
							| @ -6,7 +6,7 @@ export default class FakePatStore implements IPatStore { | |||||||
|         throw new Error('Method not implemented.'); |         throw new Error('Method not implemented.'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     delete(key: string): Promise<void> { |     delete(key: number): Promise<void> { | ||||||
|         throw new Error('Method not implemented.'); |         throw new Error('Method not implemented.'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -16,11 +16,11 @@ export default class FakePatStore implements IPatStore { | |||||||
| 
 | 
 | ||||||
|     destroy(): void {} |     destroy(): void {} | ||||||
| 
 | 
 | ||||||
|     exists(key: string): Promise<boolean> { |     exists(key: number): Promise<boolean> { | ||||||
|         throw new Error('Method not implemented.'); |         throw new Error('Method not implemented.'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     get(key: string): Promise<IPat> { |     get(key: number): Promise<IPat> { | ||||||
|         throw new Error('Method not implemented.'); |         throw new Error('Method not implemented.'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -31,4 +31,8 @@ export default class FakePatStore implements IPatStore { | |||||||
|     getAllByUser(userId: number): Promise<IPat[]> { |     getAllByUser(userId: number): Promise<IPat[]> { | ||||||
|         throw new Error('Method not implemented.'); |         throw new Error('Method not implemented.'); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     deleteForUser(id: number, userId: number): Promise<void> { | ||||||
|  |         throw new Error('Method not implemented.'); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user