1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-28 00:17:12 +01:00

Personal access tokens backend (#2064)

* First version ready

* Final

* Refactor

* Update pat store

* Website revert

* Website revert

* Update

* Revert website

* Revert docs to main

* Revert docs to main

* Fix eslint

* Test

* Fix table name
This commit is contained in:
sjaanus 2022-09-16 10:54:27 +03:00 committed by GitHub
parent 26c88ff6aa
commit 1cf42d6527
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 664 additions and 34 deletions

View File

@ -3,11 +3,12 @@ docker
bundle.js bundle.js
website/blog website/blog
website/build website/build
website/core
website/docs
website/node_modules website/node_modules
website/i18n/*.js website/i18n/*.js
website/translated_docs
website/core
website/pages website/pages
website/translated_docs
website website
setupJest.js setupJest.js
frontend frontend

View File

@ -1 +1,2 @@
CHANGELOG.md CHANGELOG.md
website/docs

View File

@ -30,6 +30,7 @@ import UserSplashStore from './user-splash-store';
import RoleStore from './role-store'; import RoleStore from './role-store';
import SegmentStore from './segment-store'; import SegmentStore from './segment-store';
import GroupStore from './group-store'; import GroupStore from './group-store';
import PatStore from './pat-store';
import { PublicSignupTokenStore } from './public-signup-token-store'; import { PublicSignupTokenStore } from './public-signup-token-store';
export const createStores = ( export const createStores = (
@ -90,6 +91,7 @@ export const createStores = (
eventBus, eventBus,
getLogger, getLogger,
), ),
patStore: new PatStore(db, getLogger),
}; };
}; };

87
src/lib/db/pat-store.ts Normal file
View File

@ -0,0 +1,87 @@
import { Knex } from 'knex';
import { Logger, LogProvider } from '../logger';
import { IPatStore } from '../types/stores/pat-store';
import Pat, { IPat } from '../types/models/pat';
import NotFoundError from '../error/notfound-error';
const TABLE = 'personal_access_tokens';
const PAT_COLUMNS = [
'secret',
'user_id',
'expires_at',
'created_at',
'seen_at',
];
const fromRow = (row) => {
if (!row) {
throw new NotFoundError('No PAT found');
}
return new Pat({
secret: row.secret,
userId: row.user_id,
createdAt: row.created_at,
seenAt: row.seen_at,
expiresAt: row.expires_at,
});
};
const toRow = (user: IPat) => ({
secret: user.secret,
user_id: user.userId,
expires_at: user.expiresAt,
});
export default class PatStore implements IPatStore {
private db: Knex;
private logger: Logger;
constructor(db: Knex, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('pat-store.ts');
}
async create(token: IPat): Promise<IPat> {
const row = await this.db(TABLE).insert(toRow(token)).returning('*');
return fromRow(row[0]);
}
async delete(secret: string): Promise<void> {
return this.db(TABLE).where({ secret: secret }).del();
}
async deleteAll(): Promise<void> {
await this.db(TABLE).del();
}
destroy(): void {}
async exists(secret: string): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`,
[secret],
);
const { present } = result.rows[0];
return present;
}
async get(secret: string): Promise<Pat> {
const row = await this.db(TABLE).where({ secret }).first();
return fromRow(row);
}
async getAll(): Promise<Pat[]> {
const groups = await this.db.select(PAT_COLUMNS).from(TABLE);
return groups.map(fromRow);
}
async getAllByUser(userId: number): Promise<Pat[]> {
const groups = await this.db
.select(PAT_COLUMNS)
.from(TABLE)
.where('user_id', userId);
return groups.map(fromRow);
}
}

View File

@ -4,8 +4,8 @@ import { Logger, LogProvider } from '../logger';
import { Knex } from 'knex'; import { Knex } from 'knex';
import EventEmitter from 'events'; import EventEmitter from 'events';
import NotFoundError from '../error/notfound-error'; import NotFoundError from '../error/notfound-error';
import User from '../types/user';
import { PartialSome } from '../types/partial'; import { PartialSome } from '../types/partial';
import User from '../types/user';
const T = { const T = {
segments: 'segments', segments: 'segments',

View File

@ -113,6 +113,8 @@ import { proxyMetricsSchema } from './spec/proxy-metrics-schema';
import { setUiConfigSchema } from './spec/set-ui-config-schema'; import { setUiConfigSchema } from './spec/set-ui-config-schema';
import { edgeTokenSchema } from './spec/edge-token-schema'; import { edgeTokenSchema } from './spec/edge-token-schema';
import { validateEdgeTokensSchema } from './spec/validate-edge-tokens-schema'; import { validateEdgeTokensSchema } from './spec/validate-edge-tokens-schema';
import { patsSchema } from './spec/pats-schema';
import { patSchema } from './spec/pat-schema';
import { publicSignupTokenCreateSchema } from './spec/public-signup-token-create-schema'; import { publicSignupTokenCreateSchema } from './spec/public-signup-token-create-schema';
import { publicSignupTokenSchema } from './spec/public-signup-token-schema'; import { publicSignupTokenSchema } from './spec/public-signup-token-schema';
import { publicSignupTokensSchema } from './spec/public-signup-tokens-schema'; import { publicSignupTokensSchema } from './spec/public-signup-tokens-schema';
@ -178,6 +180,8 @@ export const schemas = {
overrideSchema, overrideSchema,
parametersSchema, parametersSchema,
passwordSchema, passwordSchema,
patSchema,
patsSchema,
patchesSchema, patchesSchema,
patchSchema, patchSchema,
permissionSchema, permissionSchema,

View File

@ -0,0 +1,31 @@
import { FromSchema } from 'json-schema-to-ts';
export const patSchema = {
$id: '#/components/schemas/patSchema',
type: 'object',
properties: {
secret: {
type: 'string',
},
expiresAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
createdAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
seenAt: {
type: 'string',
format: 'date-time',
nullable: true,
},
},
components: {
schemas: {},
},
} as const;
export type PatSchema = FromSchema<typeof patSchema>;

View File

@ -0,0 +1,22 @@
import { FromSchema } from 'json-schema-to-ts';
import { patSchema } from './pat-schema';
export const patsSchema = {
$id: '#/components/schemas/patsSchema',
type: 'object',
properties: {
pats: {
type: 'array',
items: {
$ref: '#/components/schemas/patSchema',
},
},
},
components: {
schemas: {
patSchema,
},
},
} as const;
export type PatsSchema = FromSchema<typeof patsSchema>;

View File

@ -7,7 +7,7 @@ import StrategyController from './strategy';
import EventController from './event'; import EventController from './event';
import PlaygroundController from './playground'; import PlaygroundController from './playground';
import MetricsController from './metrics'; import MetricsController from './metrics';
import UserController from './user'; import UserController from './user/user';
import ConfigController from './config'; import ConfigController from './config';
import { ContextController } from './context'; import { ContextController } from './context';
import ClientMetricsController from './client-metrics'; import ClientMetricsController from './client-metrics';
@ -23,6 +23,7 @@ import UserSplashController from './user-splash';
import ProjectApi from './project'; import ProjectApi from './project';
import { EnvironmentsController } from './environments'; import { EnvironmentsController } from './environments';
import ConstraintsController from './constraints'; import ConstraintsController from './constraints';
import PatController from './user/pat';
import { PublicSignupController } from './public-signup'; import { PublicSignupController } from './public-signup';
import { conditionalMiddleware } from '../../middleware/conditional-middleware'; import { conditionalMiddleware } from '../../middleware/conditional-middleware';
@ -61,6 +62,10 @@ class AdminApi extends Controller {
new ClientMetricsController(config, services).router, new ClientMetricsController(config, services).router,
); );
this.app.use('/user', new UserController(config, services).router); this.app.use('/user', new UserController(config, services).router);
this.app.use(
'/user/tokens',
new PatController(config, services).router,
);
this.app.use( this.app.use(
'/ui-config', '/ui-config',
new ConfigController(config, services).router, new ConfigController(config, services).router,

View File

@ -0,0 +1,105 @@
import { Response } from 'express';
import Controller from '../../controller';
import { Logger } from '../../../logger';
import { IUnleashConfig, IUnleashServices } from '../../../types';
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import { OpenApiService } from '../../../services/openapi-service';
import { emptyResponse } from '../../../openapi/util/standard-responses';
import PatService from '../../../services/pat-service';
import { NONE } from '../../../types/permissions';
import { IAuthRequest } from '../../unleash-types';
import { serializeDates } from '../../../types/serialize-dates';
import { PatSchema, patSchema } from '../../../openapi/spec/pat-schema';
import { patsSchema } from '../../../openapi/spec/pats-schema';
export default class PatController extends Controller {
private patService: PatService;
private openApiService: OpenApiService;
private logger: Logger;
constructor(
config: IUnleashConfig,
{
openApiService,
patService,
}: Pick<IUnleashServices, 'openApiService' | 'patService'>,
) {
super(config);
this.logger = config.getLogger('lib/routes/auth/pat-controller.ts');
this.openApiService = openApiService;
this.patService = patService;
this.route({
method: 'get',
path: '',
handler: this.getPats,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getPats',
responses: { 200: createResponseSchema('patsSchema') },
}),
],
});
this.route({
method: 'post',
path: '',
handler: this.createPat,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'createPat',
requestBody: createRequestSchema('patSchema'),
responses: { 200: createResponseSchema('patSchema') },
}),
],
});
this.route({
method: 'delete',
path: '/:secret',
acceptAnyContentType: true,
handler: this.deletePat,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'deletePat',
responses: { 200: emptyResponse },
}),
],
});
}
async createPat(req: IAuthRequest, res: Response): Promise<void> {
const pat = req.body;
const createdPat = await this.patService.createPat(pat, req.user);
this.openApiService.respondWithValidation(
201,
res,
patSchema.$id,
serializeDates(createdPat),
);
}
async getPats(req: IAuthRequest, res: Response<PatSchema>): Promise<void> {
const pats = await this.patService.getAll(req.user);
this.openApiService.respondWithValidation(200, res, patsSchema.$id, {
pats: serializeDates(pats),
});
}
async deletePat(
req: IAuthRequest<{ secret: string }>,
res: Response,
): Promise<void> {
const { secret } = req.params;
await this.patService.deletePat(secret);
res.status(200).end();
}
}

View File

@ -1,10 +1,10 @@
import supertest from 'supertest'; import supertest from 'supertest';
import { createServices } from '../../services'; import { createServices } from '../../../services';
import { createTestConfig } from '../../../test/config/test-config'; import { createTestConfig } from '../../../../test/config/test-config';
import createStores from '../../../test/fixtures/store'; import createStores from '../../../../test/fixtures/store';
import getApp from '../../app'; import getApp from '../../../app';
import User from '../../types/user'; import User from '../../../types/user';
const currentUser = new User({ id: 1337, email: 'test@mail.com' }); const currentUser = new User({ id: 1337, email: 'test@mail.com' });

View File

@ -1,21 +1,21 @@
import { Response } from 'express'; import { Response } from 'express';
import { IAuthRequest } from '../unleash-types'; import { IAuthRequest } from '../../unleash-types';
import Controller from '../controller'; import Controller from '../../controller';
import { AccessService } from '../../services/access-service'; import { AccessService } from '../../../services/access-service';
import { IAuthType, IUnleashConfig } from '../../types/option'; import { IAuthType, IUnleashConfig } from '../../../types/option';
import { IUnleashServices } from '../../types/services'; import { IUnleashServices } from '../../../types/services';
import UserService from '../../services/user-service'; import UserService from '../../../services/user-service';
import UserFeedbackService from '../../services/user-feedback-service'; import UserFeedbackService from '../../../services/user-feedback-service';
import UserSplashService from '../../services/user-splash-service'; import UserSplashService from '../../../services/user-splash-service';
import { ADMIN, NONE } from '../../types/permissions'; import { ADMIN, NONE } from '../../../types/permissions';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import { meSchema, MeSchema } from '../../openapi/spec/me-schema'; import { meSchema, MeSchema } from '../../../openapi/spec/me-schema';
import { serializeDates } from '../../types/serialize-dates'; import { serializeDates } from '../../../types/serialize-dates';
import { IUserPermission } from '../../types/stores/access-store'; import { IUserPermission } from '../../../types/stores/access-store';
import { PasswordSchema } from '../../openapi/spec/password-schema'; import { PasswordSchema } from '../../../openapi/spec/password-schema';
import { emptyResponse } from '../../openapi/util/standard-responses'; import { emptyResponse } from '../../../openapi/util/standard-responses';
class UserController extends Controller { class UserController extends Controller {
private accessService: AccessService; private accessService: AccessService;

View File

@ -2,8 +2,7 @@ import Controller from '../controller';
import FeatureController from './feature'; import FeatureController from './feature';
import MetricsController from './metrics'; import MetricsController from './metrics';
import RegisterController from './register'; import RegisterController from './register';
import { IUnleashConfig } from '../../types/option'; import { IUnleashConfig, IUnleashServices } from '../../types';
import { IUnleashServices } from '../../types';
export default class ClientApi extends Controller { export default class ClientApi extends Controller {
constructor(config: IUnleashConfig, services: IUnleashServices) { constructor(config: IUnleashConfig, services: IUnleashServices) {

View File

@ -1,8 +1,7 @@
import { BackstageController } from './backstage'; import { BackstageController } from './backstage';
import ResetPasswordController from './auth/reset-password-controller'; import ResetPasswordController from './auth/reset-password-controller';
import { SimplePasswordProvider } from './auth/simple-password-provider'; import { SimplePasswordProvider } from './auth/simple-password-provider';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig, IUnleashServices } from '../types';
import { IUnleashServices } from '../types/services';
import LogoutController from './logout'; import LogoutController from './logout';
const AdminApi = require('./admin-api'); const AdminApi = require('./admin-api');
@ -28,6 +27,7 @@ class IndexRouter extends Controller {
'/auth/reset', '/auth/reset',
new ResetPasswordController(config, services).router, new ResetPasswordController(config, services).router,
); );
this.use('/api/admin', new AdminApi(config, services).router); this.use('/api/admin', new AdminApi(config, services).router);
this.use('/api/client', new ClientApi(config, services).router); this.use('/api/client', new ClientApi(config, services).router);

View File

@ -1,6 +1,4 @@
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig, IUnleashStores, IUnleashServices } from '../types';
import { IUnleashStores } from '../types/stores';
import { IUnleashServices } from '../types/services';
import FeatureTypeService from './feature-type-service'; import FeatureTypeService from './feature-type-service';
import EventService from './event-service'; import EventService from './event-service';
import HealthService from './health-service'; import HealthService from './health-service';
@ -35,6 +33,7 @@ import { PlaygroundService } from './playground-service';
import { GroupService } from './group-service'; import { GroupService } from './group-service';
import { ProxyService } from './proxy-service'; import { ProxyService } from './proxy-service';
import EdgeService from './edge-service'; import EdgeService from './edge-service';
import PatService from './pat-service';
import { PublicSignupTokenService } from './public-signup-token-service'; import { PublicSignupTokenService } from './public-signup-token-service';
export const createServices = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
@ -102,6 +101,8 @@ export const createServices = (
const edgeService = new EdgeService(stores, config); const edgeService = new EdgeService(stores, config);
const patService = new PatService(stores, config);
const publicSignupTokenService = new PublicSignupTokenService( const publicSignupTokenService = new PublicSignupTokenService(
stores, stores,
config, config,
@ -143,6 +144,7 @@ export const createServices = (
groupService, groupService,
proxyService, proxyService,
edgeService, edgeService,
patService,
publicSignupTokenService, publicSignupTokenService,
}; };
}; };

View File

@ -0,0 +1,63 @@
import { IUnleashConfig, IUnleashStores } from '../types';
import { Logger } from '../logger';
import { IPatStore } from '../types/stores/pat-store';
import { IEventStore } from '../types/stores/event-store';
import { PAT_CREATED } from '../types/events';
import { IPat } from '../types/models/pat';
import crypto from 'crypto';
import User from '../types/user';
export default class PatService {
private config: IUnleashConfig;
private logger: Logger;
private patStore: IPatStore;
private eventStore: IEventStore;
constructor(
{
patStore,
eventStore,
}: Pick<IUnleashStores, 'patStore' | 'eventStore'>,
config: IUnleashConfig,
) {
this.config = config;
this.logger = config.getLogger('services/pat-service.ts');
this.patStore = patStore;
this.eventStore = eventStore;
}
async createPat(pat: IPat, user: User): Promise<IPat> {
if (new Date(pat.expiresAt) < new Date()) {
throw new Error('The expiry date should be in future.');
}
pat.secret = this.generateSecretKey();
pat.userId = user.id;
const newPat = await this.patStore.create(pat);
await this.eventStore.store({
type: PAT_CREATED,
createdBy: user.email || user.username,
data: pat,
});
return newPat;
}
async getAll(user: User): Promise<IPat[]> {
return this.patStore.getAllByUser(user.id);
}
async deletePat(secret: string): Promise<void> {
return this.patStore.delete(secret);
}
private generateSecretKey() {
const randomStr = crypto.randomBytes(28).toString('hex');
return `user:${randomStr}`;
}
}
module.exports = PatService;

View File

@ -76,6 +76,8 @@ export const SETTING_DELETED = 'setting-deleted';
export const CLIENT_METRICS = 'client-metrics'; export const CLIENT_METRICS = 'client-metrics';
export const CLIENT_REGISTER = 'client-register'; export const CLIENT_REGISTER = 'client-register';
export const PAT_CREATED = 'pat-created';
export const PUBLIC_SIGNUP_TOKEN_CREATED = 'public-signup-token-created'; export const PUBLIC_SIGNUP_TOKEN_CREATED = 'public-signup-token-created';
export const PUBLIC_SIGNUP_TOKEN_USER_ADDED = 'public-signup-token-user-added'; export const PUBLIC_SIGNUP_TOKEN_USER_ADDED = 'public-signup-token-user-added';
export const PUBLIC_SIGNUP_TOKEN_MANUALLY_EXPIRED = export const PUBLIC_SIGNUP_TOKEN_MANUALLY_EXPIRED =

View File

@ -0,0 +1,27 @@
export interface IPat {
secret: string;
userId: number;
expiresAt?: Date;
createdAt?: Date;
seenAt?: Date;
}
export default class Pat implements IPat {
secret: string;
userId: number;
expiresAt: Date;
seenAt: Date;
createdAt: Date;
constructor({ secret, userId, expiresAt, seenAt, createdAt }: IPat) {
this.secret = secret;
this.userId = userId;
this.expiresAt = expiresAt;
this.seenAt = seenAt;
this.createdAt = createdAt;
}
}

View File

@ -31,6 +31,7 @@ import { PlaygroundService } from 'lib/services/playground-service';
import { GroupService } from '../services/group-service'; import { GroupService } from '../services/group-service';
import { ProxyService } from '../services/proxy-service'; import { ProxyService } from '../services/proxy-service';
import EdgeService from '../services/edge-service'; import EdgeService from '../services/edge-service';
import PatService from '../services/pat-service';
import { PublicSignupTokenService } from '../services/public-signup-token-service'; import { PublicSignupTokenService } from '../services/public-signup-token-service';
export interface IUnleashServices { export interface IUnleashServices {
@ -69,4 +70,5 @@ export interface IUnleashServices {
segmentService: SegmentService; segmentService: SegmentService;
openApiService: OpenApiService; openApiService: OpenApiService;
clientSpecService: ClientSpecService; clientSpecService: ClientSpecService;
patService: PatService;
} }

View File

@ -26,6 +26,7 @@ import { IUserSplashStore } from './stores/user-splash-store';
import { IRoleStore } from './stores/role-store'; import { IRoleStore } from './stores/role-store';
import { ISegmentStore } from './stores/segment-store'; import { ISegmentStore } from './stores/segment-store';
import { IGroupStore } from './stores/group-store'; import { IGroupStore } from './stores/group-store';
import { IPatStore } from './stores/pat-store';
import { IPublicSignupTokenStore } from './stores/public-signup-token-store'; import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
export interface IUnleashStores { export interface IUnleashStores {
@ -57,5 +58,6 @@ export interface IUnleashStores {
userSplashStore: IUserSplashStore; userSplashStore: IUserSplashStore;
roleStore: IRoleStore; roleStore: IRoleStore;
segmentStore: ISegmentStore; segmentStore: ISegmentStore;
patStore: IPatStore;
publicSignupTokenStore: IPublicSignupTokenStore; publicSignupTokenStore: IPublicSignupTokenStore;
} }

View File

@ -0,0 +1,7 @@
import { Store } from './store';
import { IPat } from '../models/pat';
export interface IPatStore extends Store<IPat, string> {
create(group: IPat): Promise<IPat>;
getAllByUser(userId: number): Promise<IPat[]>;
}

View File

@ -0,0 +1,19 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
CREATE TABLE personal_access_tokens (
secret text not null primary key,
user_id integer not null references users (id) ON DELETE CASCADE,
expires_at timestamp with time zone NOT NULL,
seen_at timestamp with time zone,
created_at timestamp with time zone not null DEFAULT now()
);`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(`drop table personal_access_tokens`, cb);
};

View File

@ -1649,6 +1649,29 @@ exports[`should serve the OpenAPI spec 1`] = `
], ],
"type": "object", "type": "object",
}, },
"patSchema": {
"properties": {
"createdAt": {
"format": "date-time",
"nullable": true,
"type": "string",
},
"expiresAt": {
"format": "date-time",
"nullable": true,
"type": "string",
},
"secret": {
"type": "string",
},
"seenAt": {
"format": "date-time",
"nullable": true,
"type": "string",
},
},
"type": "object",
},
"patchSchema": { "patchSchema": {
"properties": { "properties": {
"from": { "from": {
@ -1681,6 +1704,17 @@ exports[`should serve the OpenAPI spec 1`] = `
}, },
"type": "array", "type": "array",
}, },
"patsSchema": {
"properties": {
"pats": {
"items": {
"$ref": "#/components/schemas/patSchema",
},
"type": "array",
},
},
"type": "object",
},
"permissionSchema": { "permissionSchema": {
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@ -6886,6 +6920,78 @@ If the provided project does not exist, the list of events will be empty.",
], ],
}, },
}, },
"/api/admin/user/tokens": {
"get": {
"operationId": "getPats",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/patsSchema",
},
},
},
"description": "patsSchema",
},
},
"tags": [
"admin",
],
},
"post": {
"operationId": "createPat",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/patSchema",
},
},
},
"description": "patSchema",
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/patSchema",
},
},
},
"description": "patSchema",
},
},
"tags": [
"admin",
],
},
},
"/api/admin/user/tokens/{secret}": {
"delete": {
"operationId": "deletePat",
"parameters": [
{
"in": "path",
"name": "secret",
"required": true,
"schema": {
"type": "string",
},
},
],
"responses": {
"200": {
"description": "This response has no body.",
},
},
"tags": [
"admin",
],
},
},
"/api/client/features": { "/api/client/features": {
"get": { "get": {
"operationId": "getAllClientFeatures", "operationId": "getAllClientFeatures",

View File

@ -0,0 +1,107 @@
import { IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { IPat } from '../../../../lib/types/models/pat';
let app: IUnleashTest;
let db: ITestDb;
let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
beforeAll(async () => {
db = await dbInit('user_pat', getLogger);
app = await setupAppWithAuth(db.stores);
await app.request
.post(`/auth/demo/login`)
.send({
email: 'user@getunleash.io',
})
.expect(200);
});
afterAll(async () => {
await app.destroy();
});
test('should create a PAT', async () => {
const { request } = app;
const { body } = await request
.post('/api/admin/user/tokens')
.send({
expiresAt: tomorrow,
} as IPat)
.set('Content-Type', 'application/json')
.expect(201);
expect(new Date(body.expiresAt)).toEqual(tomorrow);
});
test('should delete the PAT', async () => {
const { request } = app;
const response = await request
.post('/api/admin/user/tokens')
.send({
expiresAt: tomorrow,
} as IPat)
.set('Content-Type', 'application/json')
.expect(201);
const createdSecret = response.body.secret;
await request.delete(`/api/admin/user/tokens/${createdSecret}`).expect(200);
});
test('should get all PATs', async () => {
const { request } = app;
const { body } = await request
.get('/api/admin/user/tokens')
.expect('Content-Type', /json/)
.expect(200);
expect(body.pats).toHaveLength(1);
});
test('should get only current user PATs', async () => {
const { request } = app;
await app.request
.post(`/auth/demo/login`)
.send({
email: 'user-second@getunleash.io',
})
.expect(200);
await request
.post('/api/admin/user/tokens')
.send({
expiresAt: tomorrow,
} as IPat)
.set('Content-Type', 'application/json')
.expect(201);
const { body } = await request
.get('/api/admin/user/tokens')
.expect('Content-Type', /json/)
.expect(200);
expect(body.pats).toHaveLength(1);
});
test('should fail creation of PAT with passed expiry', async () => {
const { request } = app;
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
await request
.post('/api/admin/user/tokens')
.send({
expiresAt: yesterday,
} as IPat)
.set('Content-Type', 'application/json')
.expect(500);
});

34
src/test/fixtures/fake-pat-store.ts vendored Normal file
View File

@ -0,0 +1,34 @@
import { IPatStore } from '../../lib/types/stores/pat-store';
import { IPat } from '../../lib/types/models/pat';
/* eslint-disable @typescript-eslint/no-unused-vars */
export default class FakePatStore implements IPatStore {
create(group: IPat): Promise<IPat> {
throw new Error('Method not implemented.');
}
delete(key: string): Promise<void> {
throw new Error('Method not implemented.');
}
deleteAll(): Promise<void> {
throw new Error('Method not implemented.');
}
destroy(): void {}
exists(key: string): Promise<boolean> {
throw new Error('Method not implemented.');
}
get(key: string): Promise<IPat> {
throw new Error('Method not implemented.');
}
getAll(query?: Object): Promise<IPat[]> {
throw new Error('Method not implemented.');
}
getAllByUser(userId: number): Promise<IPat[]> {
throw new Error('Method not implemented.');
}
}

View File

@ -27,6 +27,7 @@ import FakeUserSplashStore from './fake-user-splash-store';
import FakeRoleStore from './fake-role-store'; import FakeRoleStore from './fake-role-store';
import FakeSegmentStore from './fake-segment-store'; import FakeSegmentStore from './fake-segment-store';
import FakeGroupStore from './fake-group-store'; import FakeGroupStore from './fake-group-store';
import FakePatStore from './fake-pat-store';
import FakePublicSignupStore from './fake-public-signup-store'; import FakePublicSignupStore from './fake-public-signup-store';
const createStores: () => IUnleashStores = () => { const createStores: () => IUnleashStores = () => {
@ -66,6 +67,7 @@ const createStores: () => IUnleashStores = () => {
roleStore: new FakeRoleStore(), roleStore: new FakeRoleStore(),
segmentStore: new FakeSegmentStore(), segmentStore: new FakeSegmentStore(),
groupStore: new FakeGroupStore(), groupStore: new FakeGroupStore(),
patStore: new FakePatStore(),
publicSignupTokenStore: new FakePublicSignupStore(), publicSignupTokenStore: new FakePublicSignupStore(),
}; };
}; };