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
|
||||
website/blog
|
||||
website/build
|
||||
website/core
|
||||
website/docs
|
||||
website/node_modules
|
||||
website/i18n/*.js
|
||||
website/translated_docs
|
||||
website/core
|
||||
website/pages
|
||||
website/translated_docs
|
||||
website
|
||||
setupJest.js
|
||||
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 SegmentStore from './segment-store';
|
||||
import GroupStore from './group-store';
|
||||
import PatStore from './pat-store';
|
||||
import { PublicSignupTokenStore } from './public-signup-token-store';
|
||||
|
||||
export const createStores = (
|
||||
@ -90,6 +91,7 @@ export const createStores = (
|
||||
eventBus,
|
||||
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 EventEmitter from 'events';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import User from '../types/user';
|
||||
import { PartialSome } from '../types/partial';
|
||||
import User from '../types/user';
|
||||
|
||||
const T = {
|
||||
segments: 'segments',
|
||||
|
@ -113,6 +113,8 @@ import { proxyMetricsSchema } from './spec/proxy-metrics-schema';
|
||||
import { setUiConfigSchema } from './spec/set-ui-config-schema';
|
||||
import { edgeTokenSchema } from './spec/edge-token-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 { publicSignupTokenSchema } from './spec/public-signup-token-schema';
|
||||
import { publicSignupTokensSchema } from './spec/public-signup-tokens-schema';
|
||||
@ -178,6 +180,8 @@ export const schemas = {
|
||||
overrideSchema,
|
||||
parametersSchema,
|
||||
passwordSchema,
|
||||
patSchema,
|
||||
patsSchema,
|
||||
patchesSchema,
|
||||
patchSchema,
|
||||
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 PlaygroundController from './playground';
|
||||
import MetricsController from './metrics';
|
||||
import UserController from './user';
|
||||
import UserController from './user/user';
|
||||
import ConfigController from './config';
|
||||
import { ContextController } from './context';
|
||||
import ClientMetricsController from './client-metrics';
|
||||
@ -23,6 +23,7 @@ import UserSplashController from './user-splash';
|
||||
import ProjectApi from './project';
|
||||
import { EnvironmentsController } from './environments';
|
||||
import ConstraintsController from './constraints';
|
||||
import PatController from './user/pat';
|
||||
import { PublicSignupController } from './public-signup';
|
||||
import { conditionalMiddleware } from '../../middleware/conditional-middleware';
|
||||
|
||||
@ -61,6 +62,10 @@ class AdminApi extends Controller {
|
||||
new ClientMetricsController(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(
|
||||
'/ui-config',
|
||||
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 { createServices } from '../../services';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { createServices } from '../../../services';
|
||||
import { createTestConfig } from '../../../../test/config/test-config';
|
||||
|
||||
import createStores from '../../../test/fixtures/store';
|
||||
import getApp from '../../app';
|
||||
import User from '../../types/user';
|
||||
import createStores from '../../../../test/fixtures/store';
|
||||
import getApp from '../../../app';
|
||||
import User from '../../../types/user';
|
||||
|
||||
const currentUser = new User({ id: 1337, email: 'test@mail.com' });
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { Response } from 'express';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import Controller from '../controller';
|
||||
import { AccessService } from '../../services/access-service';
|
||||
import { IAuthType, IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import UserService from '../../services/user-service';
|
||||
import UserFeedbackService from '../../services/user-feedback-service';
|
||||
import UserSplashService from '../../services/user-splash-service';
|
||||
import { ADMIN, NONE } from '../../types/permissions';
|
||||
import { OpenApiService } from '../../services/openapi-service';
|
||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||
import { meSchema, MeSchema } from '../../openapi/spec/me-schema';
|
||||
import { serializeDates } from '../../types/serialize-dates';
|
||||
import { IUserPermission } from '../../types/stores/access-store';
|
||||
import { PasswordSchema } from '../../openapi/spec/password-schema';
|
||||
import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||
import { IAuthRequest } from '../../unleash-types';
|
||||
import Controller from '../../controller';
|
||||
import { AccessService } from '../../../services/access-service';
|
||||
import { IAuthType, IUnleashConfig } from '../../../types/option';
|
||||
import { IUnleashServices } from '../../../types/services';
|
||||
import UserService from '../../../services/user-service';
|
||||
import UserFeedbackService from '../../../services/user-feedback-service';
|
||||
import UserSplashService from '../../../services/user-splash-service';
|
||||
import { ADMIN, NONE } from '../../../types/permissions';
|
||||
import { OpenApiService } from '../../../services/openapi-service';
|
||||
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
|
||||
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
|
||||
import { meSchema, MeSchema } from '../../../openapi/spec/me-schema';
|
||||
import { serializeDates } from '../../../types/serialize-dates';
|
||||
import { IUserPermission } from '../../../types/stores/access-store';
|
||||
import { PasswordSchema } from '../../../openapi/spec/password-schema';
|
||||
import { emptyResponse } from '../../../openapi/util/standard-responses';
|
||||
|
||||
class UserController extends Controller {
|
||||
private accessService: AccessService;
|
@ -2,8 +2,7 @@ import Controller from '../controller';
|
||||
import FeatureController from './feature';
|
||||
import MetricsController from './metrics';
|
||||
import RegisterController from './register';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types';
|
||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||
|
||||
export default class ClientApi extends Controller {
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { BackstageController } from './backstage';
|
||||
import ResetPasswordController from './auth/reset-password-controller';
|
||||
import { SimplePasswordProvider } from './auth/simple-password-provider';
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { IUnleashServices } from '../types/services';
|
||||
import { IUnleashConfig, IUnleashServices } from '../types';
|
||||
import LogoutController from './logout';
|
||||
|
||||
const AdminApi = require('./admin-api');
|
||||
@ -28,6 +27,7 @@ class IndexRouter extends Controller {
|
||||
'/auth/reset',
|
||||
new ResetPasswordController(config, services).router,
|
||||
);
|
||||
|
||||
this.use('/api/admin', new AdminApi(config, services).router);
|
||||
this.use('/api/client', new ClientApi(config, services).router);
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { IUnleashStores } from '../types/stores';
|
||||
import { IUnleashServices } from '../types/services';
|
||||
import { IUnleashConfig, IUnleashStores, IUnleashServices } from '../types';
|
||||
import FeatureTypeService from './feature-type-service';
|
||||
import EventService from './event-service';
|
||||
import HealthService from './health-service';
|
||||
@ -35,6 +33,7 @@ import { PlaygroundService } from './playground-service';
|
||||
import { GroupService } from './group-service';
|
||||
import { ProxyService } from './proxy-service';
|
||||
import EdgeService from './edge-service';
|
||||
import PatService from './pat-service';
|
||||
import { PublicSignupTokenService } from './public-signup-token-service';
|
||||
export const createServices = (
|
||||
stores: IUnleashStores,
|
||||
@ -102,6 +101,8 @@ export const createServices = (
|
||||
|
||||
const edgeService = new EdgeService(stores, config);
|
||||
|
||||
const patService = new PatService(stores, config);
|
||||
|
||||
const publicSignupTokenService = new PublicSignupTokenService(
|
||||
stores,
|
||||
config,
|
||||
@ -143,6 +144,7 @@ export const createServices = (
|
||||
groupService,
|
||||
proxyService,
|
||||
edgeService,
|
||||
patService,
|
||||
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_REGISTER = 'client-register';
|
||||
|
||||
export const PAT_CREATED = 'pat-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_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 { ProxyService } from '../services/proxy-service';
|
||||
import EdgeService from '../services/edge-service';
|
||||
import PatService from '../services/pat-service';
|
||||
import { PublicSignupTokenService } from '../services/public-signup-token-service';
|
||||
|
||||
export interface IUnleashServices {
|
||||
@ -69,4 +70,5 @@ export interface IUnleashServices {
|
||||
segmentService: SegmentService;
|
||||
openApiService: OpenApiService;
|
||||
clientSpecService: ClientSpecService;
|
||||
patService: PatService;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import { IUserSplashStore } from './stores/user-splash-store';
|
||||
import { IRoleStore } from './stores/role-store';
|
||||
import { ISegmentStore } from './stores/segment-store';
|
||||
import { IGroupStore } from './stores/group-store';
|
||||
import { IPatStore } from './stores/pat-store';
|
||||
import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
|
||||
|
||||
export interface IUnleashStores {
|
||||
@ -57,5 +58,6 @@ export interface IUnleashStores {
|
||||
userSplashStore: IUserSplashStore;
|
||||
roleStore: IRoleStore;
|
||||
segmentStore: ISegmentStore;
|
||||
patStore: IPatStore;
|
||||
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",
|
||||
},
|
||||
"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": {
|
||||
"properties": {
|
||||
"from": {
|
||||
@ -1681,6 +1704,17 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"patsSchema": {
|
||||
"properties": {
|
||||
"pats": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/patSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"permissionSchema": {
|
||||
"additionalProperties": false,
|
||||
"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": {
|
||||
"get": {
|
||||
"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 FakeSegmentStore from './fake-segment-store';
|
||||
import FakeGroupStore from './fake-group-store';
|
||||
import FakePatStore from './fake-pat-store';
|
||||
import FakePublicSignupStore from './fake-public-signup-store';
|
||||
|
||||
const createStores: () => IUnleashStores = () => {
|
||||
@ -66,6 +67,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
roleStore: new FakeRoleStore(),
|
||||
segmentStore: new FakeSegmentStore(),
|
||||
groupStore: new FakeGroupStore(),
|
||||
patStore: new FakePatStore(),
|
||||
publicSignupTokenStore: new FakePublicSignupStore(),
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user