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:
parent
26c88ff6aa
commit
1cf42d6527
@ -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
|
||||||
|
@ -1 +1,2 @@
|
|||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
website/docs
|
||||||
|
@ -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
87
src/lib/db/pat-store.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
31
src/lib/openapi/spec/pat-schema.ts
Normal file
31
src/lib/openapi/spec/pat-schema.ts
Normal 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>;
|
22
src/lib/openapi/spec/pats-schema.ts
Normal file
22
src/lib/openapi/spec/pats-schema.ts
Normal 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>;
|
@ -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,
|
||||||
|
105
src/lib/routes/admin-api/user/pat.ts
Normal file
105
src/lib/routes/admin-api/user/pat.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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' });
|
||||||
|
|
@ -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;
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
63
src/lib/services/pat-service.ts
Normal file
63
src/lib/services/pat-service.ts
Normal 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;
|
@ -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 =
|
||||||
|
27
src/lib/types/models/pat.ts
Normal file
27
src/lib/types/models/pat.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
7
src/lib/types/stores/pat-store.ts
Normal file
7
src/lib/types/stores/pat-store.ts
Normal 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[]>;
|
||||||
|
}
|
19
src/migrations/20220912165344-pat-tokens.js
Normal file
19
src/migrations/20220912165344-pat-tokens.js
Normal 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);
|
||||||
|
};
|
@ -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",
|
||||||
|
107
src/test/e2e/api/user/pat.e2e.test.ts
Normal file
107
src/test/e2e/api/user/pat.e2e.test.ts
Normal 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
34
src/test/fixtures/fake-pat-store.ts
vendored
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -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(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user