mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +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