mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
PublicSignupTokens (#2053)
* PublicSignupTokens * bug fix * bug fixes and test * bug fixes and test * bug fixes and test * Add feature flag * tests * tests * Update 20220908093515-add-public-signup-tokens.js Bug Fix * task: use swc instead of ts-jest (#2042) * Add a counter for total number of environments (#1964) * add groupId to gradual rollout template (#2045) * add groupId to gradual rollout template * FMT * Improve tabs UI on smaller devices (#2014) * Improve tabs UI on smaller devices * Improve tabs UI on smaller devices * bug fix * add proper scrollable tabs * removed centered from Tabs (conflicts with scrollable) * PR comments * 4.15.0-beta.10 * Fix broken doc links (#2046) ## What This PR fixes some broken links that have been hanging around in the docs for what seems like a very long time. ## Why As discovered by the link check in #1912, there are a fair few broken links in the docs. Everyone hates broken links because it makes it harder to understand what they were supposed to be pointing at. ## How There are 3 types of links that have been fixed: - Links that should have been internal but were absolute. E.g. `https://docs.getunleash.io/path/article` that should have been `./article.md` - External links that have changed, such as Slack's API description - GitHub links to files that either no longer exist or that have been moved. These links generally pointed to `master`/`main`, meaning they are subject to change. They have been replaced with permalinks pointing to specific commits. ----- * docs: fix slack api doc link * docs: update links in migration guide * docs: fix broken link to ancient feature schema validation * docs: update links to v3 auth hooks * docs: update broken link in the go sdk article * Fix: use permalink for GitHub link * docs: fix wrong google auth link * 4.15.0 * 4.15.1 * docs: update link for symfony sdk (#2048) The doc link appears to have pointed at an address that is no longer reachable. Instead, let's point to the equivalent GitHub link Relates to and closes #2047 * docs: test broken links in website (#1912) The action triggers manually as a first step to test this functionality. In the near future, we might schedule it * Schedule link checker action (#2050) Runs at 12:30 UTC on Mon, Tue, Wed, Thu and Fri * fix: add env and project labels to feature updated metrics. (#2043) * Revert workflow (#2051) * update snapshot * PR comments * Added Events and tests * Throw error if token not found Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai> Co-authored-by: Thomas Heartman <thomas@getunleash.ai> Co-authored-by: Gastón Fournier <gaston@getunleash.ai> Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: sjaanus <sellinjaanus@gmail.com>
This commit is contained in:
parent
ce3db75133
commit
6778d347cd
@ -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 { PublicSignupTokenStore } from './public-signup-token-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -84,6 +85,11 @@ export const createStores = (
|
|||||||
roleStore: new RoleStore(db, eventBus, getLogger),
|
roleStore: new RoleStore(db, eventBus, getLogger),
|
||||||
segmentStore: new SegmentStore(db, eventBus, getLogger),
|
segmentStore: new SegmentStore(db, eventBus, getLogger),
|
||||||
groupStore: new GroupStore(db),
|
groupStore: new GroupStore(db),
|
||||||
|
publicSignupTokenStore: new PublicSignupTokenStore(
|
||||||
|
db,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
197
src/lib/db/public-signup-token-store.ts
Normal file
197
src/lib/db/public-signup-token-store.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import metricsHelper from '../util/metrics-helper';
|
||||||
|
import { DB_TIME } from '../metric-events';
|
||||||
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
import NotFoundError from '../error/notfound-error';
|
||||||
|
import { PublicSignupTokenSchema } from '../openapi/spec/public-signup-token-schema';
|
||||||
|
import { IPublicSignupTokenStore } from '../types/stores/public-signup-token-store';
|
||||||
|
import { UserSchema } from '../openapi/spec/user-schema';
|
||||||
|
import { IPublicSignupTokenCreate } from '../types/models/public-signup-token';
|
||||||
|
|
||||||
|
const TABLE = 'public_signup_tokens';
|
||||||
|
const TOKEN_USERS_TABLE = 'public_signup_tokens_user';
|
||||||
|
|
||||||
|
interface ITokenInsert {
|
||||||
|
secret: string;
|
||||||
|
name: string;
|
||||||
|
expires_at: Date;
|
||||||
|
created_at: Date;
|
||||||
|
created_by?: string;
|
||||||
|
role_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITokenRow extends ITokenInsert {
|
||||||
|
users: UserSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITokenUserRow {
|
||||||
|
secret: string;
|
||||||
|
user_id: number;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
|
const tokenRowReducer = (acc, tokenRow) => {
|
||||||
|
const { userId, name, ...token } = tokenRow;
|
||||||
|
if (!acc[tokenRow.secret]) {
|
||||||
|
acc[tokenRow.secret] = {
|
||||||
|
secret: token.secret,
|
||||||
|
name: token.name,
|
||||||
|
expiresAt: token.expires_at,
|
||||||
|
createdAt: token.created_at,
|
||||||
|
createdBy: token.created_by,
|
||||||
|
roleId: token.role_id,
|
||||||
|
users: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const currentToken = acc[tokenRow.secret];
|
||||||
|
if (userId) {
|
||||||
|
currentToken.users.push({ userId, name });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toRow = (newToken: IPublicSignupTokenCreate) => {
|
||||||
|
if (!newToken) return;
|
||||||
|
return {
|
||||||
|
secret: newToken.secret,
|
||||||
|
name: newToken.name,
|
||||||
|
expires_at: newToken.expiresAt,
|
||||||
|
created_by: newToken.createdBy || null,
|
||||||
|
role_id: newToken.roleId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTokens = (rows: any[]): PublicSignupTokenSchema[] => {
|
||||||
|
const tokens = rows.reduce(tokenRowReducer, {});
|
||||||
|
return Object.values(tokens);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PublicSignupTokenStore implements IPublicSignupTokenStore {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private timer: Function;
|
||||||
|
|
||||||
|
private db: Knex;
|
||||||
|
|
||||||
|
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||||
|
this.db = db;
|
||||||
|
this.logger = getLogger('public-signup-tokens.js');
|
||||||
|
this.timer = (action: string) =>
|
||||||
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
|
store: 'public-signup-tokens',
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
count(): Promise<number> {
|
||||||
|
return this.db(TABLE)
|
||||||
|
.count('*')
|
||||||
|
.then((res) => Number(res[0].count));
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeTokenUsersQuery() {
|
||||||
|
return this.db<ITokenRow>(`${TABLE} as tokens`)
|
||||||
|
.leftJoin(
|
||||||
|
`${TOKEN_USERS_TABLE} as token_project_users`,
|
||||||
|
'tokens.secret',
|
||||||
|
'token_project_users.secret',
|
||||||
|
)
|
||||||
|
.leftJoin(`users`, 'token_project_users.user_id', 'users.id')
|
||||||
|
.select(
|
||||||
|
'tokens.secret',
|
||||||
|
'tokens.name',
|
||||||
|
'tokens.expires_at',
|
||||||
|
'tokens.created_at',
|
||||||
|
'tokens.created_by',
|
||||||
|
'tokens.role_id',
|
||||||
|
'token_project_users.user_id',
|
||||||
|
'users.name',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<PublicSignupTokenSchema[]> {
|
||||||
|
const stopTimer = this.timer('getAll');
|
||||||
|
const rows = await this.makeTokenUsersQuery();
|
||||||
|
stopTimer();
|
||||||
|
return toTokens(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllActive(): Promise<PublicSignupTokenSchema[]> {
|
||||||
|
const stopTimer = this.timer('getAllActive');
|
||||||
|
const rows = await this.makeTokenUsersQuery()
|
||||||
|
.where('expires_at', 'IS', null)
|
||||||
|
.orWhere('expires_at', '>', 'now()');
|
||||||
|
stopTimer();
|
||||||
|
return toTokens(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTokenUser(secret: string, userId: number): Promise<void> {
|
||||||
|
await this.db<ITokenUserRow>(TOKEN_USERS_TABLE).insert(
|
||||||
|
{ user_id: userId, secret },
|
||||||
|
['created_at'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(
|
||||||
|
newToken: IPublicSignupTokenCreate,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
const response = await this.db<ITokenRow>(TABLE).insert(
|
||||||
|
toRow(newToken),
|
||||||
|
['created_at'],
|
||||||
|
);
|
||||||
|
return toTokens([response])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async isValid(secret: string): Promise<boolean> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE secret = ? AND expires_at::date > ?) AS valid`,
|
||||||
|
[secret, new Date()],
|
||||||
|
);
|
||||||
|
const { valid } = result.rows[0];
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(key: string): Promise<PublicSignupTokenSchema> {
|
||||||
|
const row = await this.makeTokenUsersQuery()
|
||||||
|
.where('secret', key)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw new NotFoundError('Could not find a token with that key');
|
||||||
|
|
||||||
|
return toTokens([row])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(secret: string): Promise<void> {
|
||||||
|
return this.db<ITokenInsert>(TABLE).where({ secret }).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
return this.db<ITokenInsert>(TABLE).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setExpiry(
|
||||||
|
secret: string,
|
||||||
|
expiresAt: Date,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
const rows = await this.makeTokenUsersQuery()
|
||||||
|
.update({ expires_at: expiresAt })
|
||||||
|
.where('secret', secret)
|
||||||
|
.returning('*');
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return toTokens(rows)[0];
|
||||||
|
}
|
||||||
|
throw new NotFoundError('Could not find public signup token.');
|
||||||
|
}
|
||||||
|
}
|
@ -113,6 +113,10 @@ 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 { publicSignupTokenCreateSchema } from './spec/public-signup-token-create-schema';
|
||||||
|
import { publicSignupTokenSchema } from './spec/public-signup-token-schema';
|
||||||
|
import { publicSignupTokensSchema } from './spec/public-signup-tokens-schema';
|
||||||
|
import { publicSignupTokenUpdateSchema } from './spec/public-signup-token-update-schema';
|
||||||
|
|
||||||
// All schemas in `openapi/spec` should be listed here.
|
// All schemas in `openapi/spec` should be listed here.
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
@ -140,6 +144,7 @@ export const schemas = {
|
|||||||
createUserSchema,
|
createUserSchema,
|
||||||
dateSchema,
|
dateSchema,
|
||||||
emailSchema,
|
emailSchema,
|
||||||
|
edgeTokenSchema,
|
||||||
environmentSchema,
|
environmentSchema,
|
||||||
environmentsSchema,
|
environmentsSchema,
|
||||||
eventSchema,
|
eventSchema,
|
||||||
@ -182,6 +187,14 @@ export const schemas = {
|
|||||||
playgroundRequestSchema,
|
playgroundRequestSchema,
|
||||||
playgroundResponseSchema,
|
playgroundResponseSchema,
|
||||||
projectEnvironmentSchema,
|
projectEnvironmentSchema,
|
||||||
|
publicSignupTokenCreateSchema,
|
||||||
|
publicSignupTokenUpdateSchema,
|
||||||
|
publicSignupTokensSchema,
|
||||||
|
publicSignupTokenSchema,
|
||||||
|
proxyClientSchema,
|
||||||
|
proxyFeaturesSchema,
|
||||||
|
proxyFeatureSchema,
|
||||||
|
proxyMetricsSchema,
|
||||||
projectSchema,
|
projectSchema,
|
||||||
projectsSchema,
|
projectsSchema,
|
||||||
resetPasswordSchema,
|
resetPasswordSchema,
|
||||||
@ -219,11 +232,6 @@ export const schemas = {
|
|||||||
variantSchema,
|
variantSchema,
|
||||||
variantsSchema,
|
variantsSchema,
|
||||||
versionSchema,
|
versionSchema,
|
||||||
proxyClientSchema,
|
|
||||||
proxyFeaturesSchema,
|
|
||||||
proxyFeatureSchema,
|
|
||||||
proxyMetricsSchema,
|
|
||||||
edgeTokenSchema,
|
|
||||||
validateEdgeTokensSchema,
|
validateEdgeTokensSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
22
src/lib/openapi/spec/public-signup-schema.test.ts
Normal file
22
src/lib/openapi/spec/public-signup-schema.test.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { validateSchema } from '../validate';
|
||||||
|
import { PublicSignupTokenSchema } from './public-signup-token-schema';
|
||||||
|
|
||||||
|
test('publicSignupTokenSchema', () => {
|
||||||
|
const data: PublicSignupTokenSchema = {
|
||||||
|
name: 'Default',
|
||||||
|
secret: 'some-secret',
|
||||||
|
expiresAt: new Date().toISOString(),
|
||||||
|
users: [],
|
||||||
|
role: { name: 'Viewer ', type: 'type', id: 1 },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
createdBy: 'someone',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
validateSchema('#/components/schemas/publicSignupTokenSchema', {}),
|
||||||
|
).not.toBeUndefined();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
validateSchema('#/components/schemas/publicSignupTokenSchema', data),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
22
src/lib/openapi/spec/public-signup-token-create-schema.ts
Normal file
22
src/lib/openapi/spec/public-signup-token-create-schema.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const publicSignupTokenCreateSchema = {
|
||||||
|
$id: '#/components/schemas/publicSignupTokenCreateSchema',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['name', 'expiresAt'],
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PublicSignupTokenCreateSchema = FromSchema<
|
||||||
|
typeof publicSignupTokenCreateSchema
|
||||||
|
>;
|
50
src/lib/openapi/spec/public-signup-token-schema.ts
Normal file
50
src/lib/openapi/spec/public-signup-token-schema.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
import { userSchema } from './user-schema';
|
||||||
|
import { roleSchema } from './role-schema';
|
||||||
|
|
||||||
|
export const publicSignupTokenSchema = {
|
||||||
|
$id: '#/components/schemas/publicSignupTokenSchema',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['secret', 'name', 'expiresAt', 'createdAt', 'createdBy', 'role'],
|
||||||
|
properties: {
|
||||||
|
secret: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/userSchema',
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
$ref: '#/components/schemas/roleSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
userSchema,
|
||||||
|
roleSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PublicSignupTokenSchema = FromSchema<
|
||||||
|
typeof publicSignupTokenSchema
|
||||||
|
>;
|
19
src/lib/openapi/spec/public-signup-token-update-schema.ts
Normal file
19
src/lib/openapi/spec/public-signup-token-update-schema.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const publicSignupTokenUpdateSchema = {
|
||||||
|
$id: '#/components/schemas/publicSignupTokenUpdateSchema',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['expiresAt'],
|
||||||
|
properties: {
|
||||||
|
expiresAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PublicSignupTokenUpdateSchema = FromSchema<
|
||||||
|
typeof publicSignupTokenUpdateSchema
|
||||||
|
>;
|
30
src/lib/openapi/spec/public-signup-tokens-schema.ts
Normal file
30
src/lib/openapi/spec/public-signup-tokens-schema.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
import { userSchema } from './user-schema';
|
||||||
|
import { roleSchema } from './role-schema';
|
||||||
|
import { publicSignupTokenSchema } from './public-signup-token-schema';
|
||||||
|
|
||||||
|
export const publicSignupTokensSchema = {
|
||||||
|
$id: '#/components/schemas/publicSignupTokensSchema',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['tokens'],
|
||||||
|
properties: {
|
||||||
|
tokens: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/publicSignupTokenSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
publicSignupTokenSchema,
|
||||||
|
userSchema,
|
||||||
|
roleSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PublicSignupTokensSchema = FromSchema<
|
||||||
|
typeof publicSignupTokensSchema
|
||||||
|
>;
|
@ -61,6 +61,11 @@ const OPENAPI_TAGS = [
|
|||||||
description:
|
description:
|
||||||
'Create, update, and delete [Unleash projects](https://docs.getunleash.io/user_guide/projects).',
|
'Create, update, and delete [Unleash projects](https://docs.getunleash.io/user_guide/projects).',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Public signup tokens',
|
||||||
|
description:
|
||||||
|
'Create, update, and delete [Unleash Public Signup tokens](https://docs.getunleash.io/reference/public-signup-tokens).',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Strategies',
|
name: 'Strategies',
|
||||||
description:
|
description:
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices, IUnleashConfig } from '../../types';
|
||||||
import { IUnleashConfig } from '../../types/option';
|
|
||||||
import FeatureController from './feature';
|
import FeatureController from './feature';
|
||||||
import { FeatureTypeController } from './feature-type';
|
import { FeatureTypeController } from './feature-type';
|
||||||
import ArchiveController from './archive';
|
import ArchiveController from './archive';
|
||||||
@ -24,6 +23,8 @@ 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 { PublicSignupController } from './public-signup';
|
||||||
|
import { conditionalMiddleware } from '../../middleware/conditional-middleware';
|
||||||
|
|
||||||
class AdminApi extends Controller {
|
class AdminApi extends Controller {
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||||
@ -101,6 +102,13 @@ class AdminApi extends Controller {
|
|||||||
'/constraints',
|
'/constraints',
|
||||||
new ConstraintsController(config, services).router,
|
new ConstraintsController(config, services).router,
|
||||||
);
|
);
|
||||||
|
this.app.use(
|
||||||
|
'/invite-link',
|
||||||
|
conditionalMiddleware(
|
||||||
|
() => config.flagResolver.isEnabled('publicSignup'),
|
||||||
|
new PublicSignupController(config, services).router,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
211
src/lib/routes/admin-api/public-signup.test.ts
Normal file
211
src/lib/routes/admin-api/public-signup.test.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import createStores from '../../../test/fixtures/store';
|
||||||
|
import { createTestConfig } from '../../../test/config/test-config';
|
||||||
|
import { createServices } from '../../services';
|
||||||
|
import getApp from '../../app';
|
||||||
|
import supertest from 'supertest';
|
||||||
|
import permissions from '../../../test/fixtures/permissions';
|
||||||
|
import { RoleName, RoleType } from '../../types/model';
|
||||||
|
import { CreateUserSchema } from '../../openapi/spec/create-user-schema';
|
||||||
|
|
||||||
|
describe('Public Signup API', () => {
|
||||||
|
async function getSetup() {
|
||||||
|
const stores = createStores();
|
||||||
|
const perms = permissions();
|
||||||
|
const config = createTestConfig({
|
||||||
|
preRouterHook: perms.hook,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.flagResolver = {
|
||||||
|
isEnabled: jest.fn().mockResolvedValue(true),
|
||||||
|
getAll: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
stores.accessStore = {
|
||||||
|
...stores.accessStore,
|
||||||
|
addUserToRole: jest.fn(),
|
||||||
|
removeRolesOfTypeForUser: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = createServices(stores, config);
|
||||||
|
const app = await getApp(config, stores, services);
|
||||||
|
|
||||||
|
await stores.roleStore.create({
|
||||||
|
name: RoleName.VIEWER,
|
||||||
|
roleType: RoleType.ROOT,
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: supertest(app),
|
||||||
|
stores,
|
||||||
|
perms,
|
||||||
|
destroy: () => {
|
||||||
|
services.versionService.destroy();
|
||||||
|
services.clientInstanceService.destroy();
|
||||||
|
services.publicSignupTokenService.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let stores;
|
||||||
|
let request;
|
||||||
|
let destroy;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const setup = await getSetup();
|
||||||
|
stores = setup.stores;
|
||||||
|
request = setup.request;
|
||||||
|
destroy = setup.destroy;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
destroy();
|
||||||
|
});
|
||||||
|
const expireAt = (addDays: number = 7): Date => {
|
||||||
|
let now = new Date();
|
||||||
|
now.setDate(now.getDate() + addDays);
|
||||||
|
return now;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBody = () => ({
|
||||||
|
name: 'some-name',
|
||||||
|
expiresAt: expireAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create a token', async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
stores.roleStore.create({ name: RoleName.VIEWER });
|
||||||
|
const bodyCreate = createBody();
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post('/api/admin/invite-link/tokens')
|
||||||
|
.send(bodyCreate)
|
||||||
|
.expect(201)
|
||||||
|
.expect(async (res) => {
|
||||||
|
const token = res.body;
|
||||||
|
expect(token.name).toBe(bodyCreate.name);
|
||||||
|
expect(token.secret).not.toBeNull();
|
||||||
|
expect(token.expiresAt).toBe(
|
||||||
|
bodyCreate.expiresAt.toISOString(),
|
||||||
|
);
|
||||||
|
const eventCount = await stores.eventStore.count();
|
||||||
|
expect(eventCount).toBe(1); //PUBLIC_SIGNUP_TOKEN_CREATED
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get All', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
stores.publicSignupTokenStore.create({
|
||||||
|
name: 'some-name',
|
||||||
|
expiresAt: expireAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get('/api/admin/invite-link/tokens')
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
const { tokens } = res.body;
|
||||||
|
expect(tokens[0].name).toBe('some-name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get token', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
stores.publicSignupTokenStore.create({
|
||||||
|
name: 'some-name',
|
||||||
|
expiresAt: expireAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get('/api/admin/invite-link/tokens/some-secret')
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
const token = res.body;
|
||||||
|
expect(token.name).toBe('some-name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should expire token', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
stores.publicSignupTokenStore.create({
|
||||||
|
name: 'some-name',
|
||||||
|
expiresAt: expireAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.delete('/api/admin/invite-link/tokens/some-secret')
|
||||||
|
.expect(200)
|
||||||
|
.expect(async () => {
|
||||||
|
const eventCount = await stores.eventStore.count();
|
||||||
|
expect(eventCount).toBe(1); // PUBLIC_SIGNUP_TOKEN_MANUALLY_EXPIRED
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create user and add to token', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
stores.publicSignupTokenStore.create({
|
||||||
|
name: 'some-name',
|
||||||
|
expiresAt: expireAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const user: CreateUserSchema = {
|
||||||
|
username: 'some-username',
|
||||||
|
email: 'someEmail@example.com',
|
||||||
|
name: 'some-name',
|
||||||
|
password: null,
|
||||||
|
rootRole: 1,
|
||||||
|
sendEmail: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post('/api/admin/invite-link/tokens/some-secret/signup')
|
||||||
|
.send(user)
|
||||||
|
.expect(201)
|
||||||
|
.expect(async (res) => {
|
||||||
|
const count = await stores.userStore.count();
|
||||||
|
expect(count).toBe(1);
|
||||||
|
const eventCount = await stores.eventStore.count();
|
||||||
|
expect(eventCount).toBe(2); //USER_CREATED && PUBLIC_SIGNUP_TOKEN_USER_ADDED
|
||||||
|
expect(res.body.username).toBe(user.username);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 200 if token is valid', async () => {
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
stores.publicSignupTokenStore.create({
|
||||||
|
name: 'some-name',
|
||||||
|
expiresAt: expireAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post('/api/admin/invite-link/tokens/some-secret/validate')
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 401 if token is invalid', async () => {
|
||||||
|
const appName = '123!23';
|
||||||
|
|
||||||
|
stores.clientApplicationsStore.upsert({ appName });
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post('/api/admin/invite-link/tokens/some-invalid-secret/validate')
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
});
|
289
src/lib/routes/admin-api/public-signup.ts
Normal file
289
src/lib/routes/admin-api/public-signup.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
import Controller from '../controller';
|
||||||
|
import { ADMIN, NONE } from '../../types/permissions';
|
||||||
|
import { Logger } from '../../logger';
|
||||||
|
import { AccessService } from '../../services/access-service';
|
||||||
|
import { IAuthRequest } from '../unleash-types';
|
||||||
|
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
|
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||||
|
import { serializeDates } from '../../types/serialize-dates';
|
||||||
|
import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||||
|
import { PublicSignupTokenService } from '../../services/public-signup-token-service';
|
||||||
|
import UserService from '../../services/user-service';
|
||||||
|
import {
|
||||||
|
publicSignupTokenSchema,
|
||||||
|
PublicSignupTokenSchema,
|
||||||
|
} from '../../openapi/spec/public-signup-token-schema';
|
||||||
|
import {
|
||||||
|
publicSignupTokensSchema,
|
||||||
|
PublicSignupTokensSchema,
|
||||||
|
} from '../../openapi/spec/public-signup-tokens-schema';
|
||||||
|
import { PublicSignupTokenCreateSchema } from '../../openapi/spec/public-signup-token-create-schema';
|
||||||
|
import { PublicSignupTokenUpdateSchema } from '../../openapi/spec/public-signup-token-update-schema';
|
||||||
|
import { CreateUserSchema } from '../../openapi/spec/create-user-schema';
|
||||||
|
import { UserSchema, userSchema } from '../../openapi/spec/user-schema';
|
||||||
|
|
||||||
|
interface TokenParam {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PublicSignupController extends Controller {
|
||||||
|
private publicSignupTokenService: PublicSignupTokenService;
|
||||||
|
|
||||||
|
private userService: UserService;
|
||||||
|
|
||||||
|
private accessService: AccessService;
|
||||||
|
|
||||||
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: IUnleashConfig,
|
||||||
|
{
|
||||||
|
publicSignupTokenService,
|
||||||
|
accessService,
|
||||||
|
userService,
|
||||||
|
openApiService,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashServices,
|
||||||
|
| 'publicSignupTokenService'
|
||||||
|
| 'accessService'
|
||||||
|
| 'userService'
|
||||||
|
| 'openApiService'
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
this.publicSignupTokenService = publicSignupTokenService;
|
||||||
|
this.accessService = accessService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.openApiService = openApiService;
|
||||||
|
this.logger = config.getLogger('public-signup-controller.js');
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: '/tokens',
|
||||||
|
handler: this.getAllPublicSignupTokens,
|
||||||
|
permission: ADMIN,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Public signup tokens'],
|
||||||
|
operationId: 'getAllPublicSignupTokens',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('publicSignupTokenSchema'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '/tokens',
|
||||||
|
handler: this.createPublicSignupToken,
|
||||||
|
permission: ADMIN,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Public signup tokens'],
|
||||||
|
operationId: 'createApiToken',
|
||||||
|
requestBody: createRequestSchema(
|
||||||
|
'publicSignupTokenCreateSchema',
|
||||||
|
),
|
||||||
|
responses: {
|
||||||
|
201: createResponseSchema('publicSignupTokenSchema'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '/tokens/:token/signup',
|
||||||
|
handler: this.addTokenUser,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Public signup tokens'],
|
||||||
|
operationId: 'addPublicSignupTokenUser',
|
||||||
|
requestBody: createRequestSchema('createUserSchema'),
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('userSchema'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: '/tokens/:token',
|
||||||
|
handler: this.getPublicSignupToken,
|
||||||
|
permission: ADMIN,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Public signup tokens'],
|
||||||
|
operationId: 'getPublicSignupToken',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('publicSignupTokenSchema'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'put',
|
||||||
|
path: '/tokens/:token',
|
||||||
|
handler: this.updatePublicSignupToken,
|
||||||
|
permission: ADMIN,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Public signup tokens'],
|
||||||
|
operationId: 'updatePublicSignupToken',
|
||||||
|
requestBody: createRequestSchema(
|
||||||
|
'publicSignupTokenUpdateSchema',
|
||||||
|
),
|
||||||
|
responses: {
|
||||||
|
200: emptyResponse,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'delete',
|
||||||
|
path: '/tokens/:token',
|
||||||
|
handler: this.deletePublicSignupToken,
|
||||||
|
acceptAnyContentType: true,
|
||||||
|
permission: ADMIN,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['API tokens'],
|
||||||
|
operationId: 'deletePublicSignupToken',
|
||||||
|
responses: {
|
||||||
|
200: emptyResponse,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '/tokens/:token/validate',
|
||||||
|
handler: this.validate,
|
||||||
|
acceptAnyContentType: true,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['API tokens'],
|
||||||
|
operationId: 'validateSignupToken',
|
||||||
|
responses: {
|
||||||
|
200: emptyResponse,
|
||||||
|
401: emptyResponse,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllPublicSignupTokens(
|
||||||
|
req: IAuthRequest,
|
||||||
|
res: Response<PublicSignupTokensSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
const tokens = await this.publicSignupTokenService.getAllActiveTokens();
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
publicSignupTokensSchema.$id,
|
||||||
|
{ tokens: serializeDates(tokens) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicSignupToken(
|
||||||
|
req: IAuthRequest<TokenParam>,
|
||||||
|
res: Response<PublicSignupTokenSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
const { token } = req.params;
|
||||||
|
const result = await this.publicSignupTokenService.get(token);
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
publicSignupTokenSchema.$id,
|
||||||
|
serializeDates(result),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
req: IAuthRequest<TokenParam, void, CreateUserSchema>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { token } = req.params;
|
||||||
|
const valid = await this.publicSignupTokenService.validate(token);
|
||||||
|
if (valid) return res.status(200).end();
|
||||||
|
else return res.status(401).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTokenUser(
|
||||||
|
req: IAuthRequest<TokenParam, void, CreateUserSchema>,
|
||||||
|
res: Response<UserSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
const { token } = req.params;
|
||||||
|
const user = await this.publicSignupTokenService.addTokenUser(
|
||||||
|
token,
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
201,
|
||||||
|
res,
|
||||||
|
userSchema.$id,
|
||||||
|
serializeDates(user),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPublicSignupToken(
|
||||||
|
req: IAuthRequest<void, void, PublicSignupTokenCreateSchema>,
|
||||||
|
res: Response<PublicSignupTokenSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
const token =
|
||||||
|
await this.publicSignupTokenService.createNewPublicSignupToken(
|
||||||
|
req.body,
|
||||||
|
req.user.name,
|
||||||
|
);
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
201,
|
||||||
|
res,
|
||||||
|
publicSignupTokensSchema.$id,
|
||||||
|
serializeDates(token),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePublicSignupToken(
|
||||||
|
req: IAuthRequest<TokenParam, void, PublicSignupTokenUpdateSchema>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<any> {
|
||||||
|
const { token } = req.params;
|
||||||
|
const { expiresAt } = req.body;
|
||||||
|
|
||||||
|
if (!expiresAt) {
|
||||||
|
this.logger.error(req.body);
|
||||||
|
return res.status(400).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.publicSignupTokenService.setExpiry(
|
||||||
|
token,
|
||||||
|
new Date(expiresAt),
|
||||||
|
);
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePublicSignupToken(
|
||||||
|
req: IAuthRequest<TokenParam>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { token } = req.params;
|
||||||
|
|
||||||
|
await this.publicSignupTokenService.delete(token, req.user.name);
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,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 { PublicSignupTokenService } from './public-signup-token-service';
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -101,6 +102,12 @@ export const createServices = (
|
|||||||
|
|
||||||
const edgeService = new EdgeService(stores, config);
|
const edgeService = new EdgeService(stores, config);
|
||||||
|
|
||||||
|
const publicSignupTokenService = new PublicSignupTokenService(
|
||||||
|
stores,
|
||||||
|
config,
|
||||||
|
userService,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
addonService,
|
addonService,
|
||||||
@ -136,6 +143,7 @@ export const createServices = (
|
|||||||
groupService,
|
groupService,
|
||||||
proxyService,
|
proxyService,
|
||||||
edgeService,
|
edgeService,
|
||||||
|
publicSignupTokenService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
151
src/lib/services/public-signup-token-service.ts
Normal file
151
src/lib/services/public-signup-token-service.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import { IUnleashConfig, IUnleashStores } from '../types';
|
||||||
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
import { IPublicSignupTokenStore } from '../types/stores/public-signup-token-store';
|
||||||
|
import { PublicSignupTokenSchema } from '../openapi/spec/public-signup-token-schema';
|
||||||
|
import { IRoleStore } from '../types/stores/role-store';
|
||||||
|
import { IPublicSignupTokenCreate } from '../types/models/public-signup-token';
|
||||||
|
import { PublicSignupTokenCreateSchema } from '../openapi/spec/public-signup-token-create-schema';
|
||||||
|
import { RoleName } from '../types/model';
|
||||||
|
import { IEventStore } from '../types/stores/event-store';
|
||||||
|
import {
|
||||||
|
PublicSignupTokenCreatedEvent,
|
||||||
|
PublicSignupTokenManuallyExpiredEvent,
|
||||||
|
PublicSignupTokenUserAddedEvent,
|
||||||
|
} from '../types/events';
|
||||||
|
import UserService, { ICreateUser } from './user-service';
|
||||||
|
import { IUser } from '../types/user';
|
||||||
|
|
||||||
|
export class PublicSignupTokenService {
|
||||||
|
private store: IPublicSignupTokenStore;
|
||||||
|
|
||||||
|
private roleStore: IRoleStore;
|
||||||
|
|
||||||
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
|
private userService: UserService;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
private activeTokens: PublicSignupTokenSchema[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
publicSignupTokenStore,
|
||||||
|
roleStore,
|
||||||
|
eventStore,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashStores,
|
||||||
|
'publicSignupTokenStore' | 'roleStore' | 'eventStore'
|
||||||
|
>,
|
||||||
|
config: Pick<IUnleashConfig, 'getLogger' | 'authentication'>,
|
||||||
|
userService: UserService,
|
||||||
|
) {
|
||||||
|
this.store = publicSignupTokenStore;
|
||||||
|
this.userService = userService;
|
||||||
|
this.roleStore = roleStore;
|
||||||
|
this.eventStore = eventStore;
|
||||||
|
this.logger = config.getLogger(
|
||||||
|
'/services/public-signup-token-service.ts',
|
||||||
|
);
|
||||||
|
this.fetchActiveTokens();
|
||||||
|
this.timer = setInterval(
|
||||||
|
() => this.fetchActiveTokens(),
|
||||||
|
minutesToMilliseconds(1),
|
||||||
|
).unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchActiveTokens(): Promise<void> {
|
||||||
|
this.activeTokens = await this.getAllActiveTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(secret: string): Promise<PublicSignupTokenSchema> {
|
||||||
|
return this.store.get(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllTokens(): Promise<PublicSignupTokenSchema[]> {
|
||||||
|
return this.store.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllActiveTokens(): Promise<PublicSignupTokenSchema[]> {
|
||||||
|
return this.store.getAllActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(secret: string): Promise<boolean> {
|
||||||
|
return this.store.isValid(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setExpiry(
|
||||||
|
secret: string,
|
||||||
|
expireAt: Date,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
return this.store.setExpiry(secret, expireAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addTokenUser(
|
||||||
|
secret: string,
|
||||||
|
createUser: ICreateUser,
|
||||||
|
): Promise<IUser> {
|
||||||
|
const user = await this.userService.createUser(createUser);
|
||||||
|
await this.store.addTokenUser(secret, user.id);
|
||||||
|
await this.eventStore.store(
|
||||||
|
new PublicSignupTokenUserAddedEvent({
|
||||||
|
createdBy: 'userId',
|
||||||
|
data: { secret, userId: user.id },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(secret: string, expiredBy: string): Promise<void> {
|
||||||
|
await this.expireToken(secret);
|
||||||
|
await this.eventStore.store(
|
||||||
|
new PublicSignupTokenManuallyExpiredEvent({
|
||||||
|
createdBy: expiredBy,
|
||||||
|
data: { secret },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async expireToken(
|
||||||
|
secret: string,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
return this.store.setExpiry(secret, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createNewPublicSignupToken(
|
||||||
|
tokenCreate: PublicSignupTokenCreateSchema,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
const viewerRole = await this.roleStore.getRoleByName(RoleName.VIEWER);
|
||||||
|
const newToken: IPublicSignupTokenCreate = {
|
||||||
|
name: tokenCreate.name,
|
||||||
|
expiresAt: new Date(tokenCreate.expiresAt),
|
||||||
|
secret: this.generateSecretKey(),
|
||||||
|
roleId: viewerRole ? viewerRole.id : -1,
|
||||||
|
createdBy: createdBy,
|
||||||
|
};
|
||||||
|
const token = await this.store.insert(newToken);
|
||||||
|
this.activeTokens.push(token);
|
||||||
|
|
||||||
|
await this.eventStore.store(
|
||||||
|
new PublicSignupTokenCreatedEvent({
|
||||||
|
createdBy: createdBy,
|
||||||
|
data: newToken,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSecretKey(): string {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
}
|
@ -76,6 +76,11 @@ 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 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 =
|
||||||
|
'public-signup-token-manually-expired';
|
||||||
|
|
||||||
export interface IBaseEvent {
|
export interface IBaseEvent {
|
||||||
type: string;
|
type: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
@ -531,3 +536,30 @@ export class SettingUpdatedEvent extends BaseEvent {
|
|||||||
this.data = eventData.data;
|
this.data = eventData.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PublicSignupTokenCreatedEvent extends BaseEvent {
|
||||||
|
readonly data: any;
|
||||||
|
|
||||||
|
constructor(eventData: { createdBy: string; data: any }) {
|
||||||
|
super(PUBLIC_SIGNUP_TOKEN_CREATED, eventData.createdBy);
|
||||||
|
this.data = eventData.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PublicSignupTokenManuallyExpiredEvent extends BaseEvent {
|
||||||
|
readonly data: any;
|
||||||
|
|
||||||
|
constructor(eventData: { createdBy: string; data: any }) {
|
||||||
|
super(PUBLIC_SIGNUP_TOKEN_MANUALLY_EXPIRED, eventData.createdBy);
|
||||||
|
this.data = eventData.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PublicSignupTokenUserAddedEvent extends BaseEvent {
|
||||||
|
readonly data: any;
|
||||||
|
|
||||||
|
constructor(eventData: { createdBy: string; data: any }) {
|
||||||
|
super(PUBLIC_SIGNUP_TOKEN_USER_ADDED, eventData.createdBy);
|
||||||
|
this.data = eventData.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
13
src/lib/types/models/public-signup-token.ts
Normal file
13
src/lib/types/models/public-signup-token.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import User from '../user';
|
||||||
|
|
||||||
|
export interface IPublicSignupTokenCreate {
|
||||||
|
name: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
roleId: number;
|
||||||
|
secret: string;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicSignupToken extends IPublicSignupTokenCreate {
|
||||||
|
users: User[];
|
||||||
|
}
|
@ -115,6 +115,7 @@ export interface IUnleashOptions {
|
|||||||
disableLegacyFeaturesApi?: boolean;
|
disableLegacyFeaturesApi?: boolean;
|
||||||
inlineSegmentConstraints?: boolean;
|
inlineSegmentConstraints?: boolean;
|
||||||
clientFeatureCaching?: Partial<IClientCachingOption>;
|
clientFeatureCaching?: Partial<IClientCachingOption>;
|
||||||
|
flagResolver?: IFlagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEmailOption {
|
export interface IEmailOption {
|
||||||
|
@ -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 { PublicSignupTokenService } from '../services/public-signup-token-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -42,6 +43,7 @@ export interface IUnleashServices {
|
|||||||
emailService: EmailService;
|
emailService: EmailService;
|
||||||
environmentService: EnvironmentService;
|
environmentService: EnvironmentService;
|
||||||
eventService: EventService;
|
eventService: EventService;
|
||||||
|
edgeService: EdgeService;
|
||||||
featureTagService: FeatureTagService;
|
featureTagService: FeatureTagService;
|
||||||
featureToggleService: FeatureToggleService;
|
featureToggleService: FeatureToggleService;
|
||||||
featureToggleServiceV2: FeatureToggleService; // deprecated
|
featureToggleServiceV2: FeatureToggleService; // deprecated
|
||||||
@ -50,6 +52,9 @@ export interface IUnleashServices {
|
|||||||
healthService: HealthService;
|
healthService: HealthService;
|
||||||
projectHealthService: ProjectHealthService;
|
projectHealthService: ProjectHealthService;
|
||||||
projectService: ProjectService;
|
projectService: ProjectService;
|
||||||
|
playgroundService: PlaygroundService;
|
||||||
|
proxyService: ProxyService;
|
||||||
|
publicSignupTokenService: PublicSignupTokenService;
|
||||||
resetTokenService: ResetTokenService;
|
resetTokenService: ResetTokenService;
|
||||||
sessionService: SessionService;
|
sessionService: SessionService;
|
||||||
settingService: SettingService;
|
settingService: SettingService;
|
||||||
@ -64,7 +69,4 @@ export interface IUnleashServices {
|
|||||||
segmentService: SegmentService;
|
segmentService: SegmentService;
|
||||||
openApiService: OpenApiService;
|
openApiService: OpenApiService;
|
||||||
clientSpecService: ClientSpecService;
|
clientSpecService: ClientSpecService;
|
||||||
playgroundService: PlaygroundService;
|
|
||||||
proxyService: ProxyService;
|
|
||||||
edgeService: EdgeService;
|
|
||||||
}
|
}
|
||||||
|
@ -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 { IPublicSignupTokenStore } from './stores/public-signup-token-store';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -56,4 +57,5 @@ export interface IUnleashStores {
|
|||||||
userSplashStore: IUserSplashStore;
|
userSplashStore: IUserSplashStore;
|
||||||
roleStore: IRoleStore;
|
roleStore: IRoleStore;
|
||||||
segmentStore: ISegmentStore;
|
segmentStore: ISegmentStore;
|
||||||
|
publicSignupTokenStore: IPublicSignupTokenStore;
|
||||||
}
|
}
|
||||||
|
19
src/lib/types/stores/public-signup-token-store.ts
Normal file
19
src/lib/types/stores/public-signup-token-store.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Store } from './store';
|
||||||
|
import { PublicSignupTokenSchema } from '../../openapi/spec/public-signup-token-schema';
|
||||||
|
import { IPublicSignupTokenCreate } from '../models/public-signup-token';
|
||||||
|
|
||||||
|
export interface IPublicSignupTokenStore
|
||||||
|
extends Store<PublicSignupTokenSchema, string> {
|
||||||
|
getAllActive(): Promise<PublicSignupTokenSchema[]>;
|
||||||
|
insert(
|
||||||
|
newToken: IPublicSignupTokenCreate,
|
||||||
|
): Promise<PublicSignupTokenSchema>;
|
||||||
|
addTokenUser(secret: string, userId: number): Promise<void>;
|
||||||
|
isValid(secret): Promise<boolean>;
|
||||||
|
setExpiry(
|
||||||
|
secret: string,
|
||||||
|
expiresAt: Date,
|
||||||
|
): Promise<PublicSignupTokenSchema>;
|
||||||
|
delete(secret: string): Promise<void>;
|
||||||
|
count(): Promise<number>;
|
||||||
|
}
|
37
src/migrations/20220908093515-add-public-signup-tokens.js
Normal file
37
src/migrations/20220908093515-add-public-signup-tokens.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
create table IF NOT EXISTS public_signup_tokens
|
||||||
|
(
|
||||||
|
secret text primary key,
|
||||||
|
name text,
|
||||||
|
expires_at timestamp with time zone not null,
|
||||||
|
created_at timestamp with time zone not null default now(),
|
||||||
|
created_by text,
|
||||||
|
role_id integer not null references roles (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
create table IF NOT EXISTS public_signup_tokens_user
|
||||||
|
(
|
||||||
|
secret text not null references public_signup_tokens (secret) on DELETE CASCADE,
|
||||||
|
user_id integer not null references users (id) ON DELETE CASCADE,
|
||||||
|
created_at timestamp with time zone not null default now(),
|
||||||
|
primary key (secret, user_id)
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
DROP TABLE public_signup_tokens;
|
||||||
|
DROP TABLE public_signup_tokens_user;
|
||||||
|
DROP TABLE public_signup_tokens_role;
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
@ -2353,6 +2353,93 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"publicSignupTokenCreateSchema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"expiresAt",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"publicSignupTokenSchema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"createdBy": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"$ref": "#/components/schemas/roleSchema",
|
||||||
|
},
|
||||||
|
"secret": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/userSchema",
|
||||||
|
},
|
||||||
|
"nullable": true,
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"secret",
|
||||||
|
"name",
|
||||||
|
"expiresAt",
|
||||||
|
"createdAt",
|
||||||
|
"createdBy",
|
||||||
|
"role",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"publicSignupTokenUpdateSchema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"expiresAt",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"publicSignupTokensSchema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"tokens": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"tokens",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"resetPasswordSchema": {
|
"resetPasswordSchema": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -4346,6 +4433,205 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/admin/invite-link/tokens": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAllPublicSignupTokens",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Public signup tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "createApiToken",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/publicSignupTokenCreateSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "publicSignupTokenCreateSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Public signup tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/admin/invite-link/tokens/{token}": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deletePublicSignupToken",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "token",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"API tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getPublicSignupToken",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "token",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "publicSignupTokenSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Public signup tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updatePublicSignupToken",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "token",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/publicSignupTokenUpdateSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "publicSignupTokenUpdateSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Public signup tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/admin/invite-link/tokens/{token}/signup": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "addPublicSignupTokenUser",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "token",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/createUserSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "createUserSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/userSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "userSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Public signup tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/api/admin/invite-link/tokens/{token}/validate": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "validateSignupToken",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "token",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"API tokens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/admin/metrics/applications": {
|
"/api/admin/metrics/applications": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getApplications",
|
"operationId": "getApplications",
|
||||||
@ -7028,6 +7314,10 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
"description": "Create, update, and delete [Unleash projects](https://docs.getunleash.io/user_guide/projects).",
|
"description": "Create, update, and delete [Unleash projects](https://docs.getunleash.io/user_guide/projects).",
|
||||||
"name": "Projects",
|
"name": "Projects",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Create, update, and delete [Unleash Public Signup tokens](https://docs.getunleash.io/reference/public-signup-tokens).",
|
||||||
|
"name": "Public signup tokens",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Create, update, delete, manage [custom strategies](https://docs.getunleash.io/advanced/custom_activation_strategy).",
|
"description": "Create, update, delete, manage [custom strategies](https://docs.getunleash.io/advanced/custom_activation_strategy).",
|
||||||
"name": "Strategies",
|
"name": "Strategies",
|
||||||
|
85
src/test/fixtures/fake-public-signup-store.ts
vendored
Normal file
85
src/test/fixtures/fake-public-signup-store.ts
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { IPublicSignupTokenStore } from '../../lib/types/stores/public-signup-token-store';
|
||||||
|
import { PublicSignupTokenSchema } from '../../lib/openapi/spec/public-signup-token-schema';
|
||||||
|
import { IPublicSignupTokenCreate } from '../../lib/types/models/public-signup-token';
|
||||||
|
|
||||||
|
export default class FakePublicSignupStore implements IPublicSignupTokenStore {
|
||||||
|
tokens: PublicSignupTokenSchema[] = [];
|
||||||
|
|
||||||
|
async addTokenUser(secret: string, userId: number): Promise<void> {
|
||||||
|
this.get(secret).then((token) => token.users.push({ id: userId }));
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(secret: string): Promise<PublicSignupTokenSchema> {
|
||||||
|
const token = this.tokens.find((t) => t.secret === secret);
|
||||||
|
return Promise.resolve(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isValid(secret: string): Promise<boolean> {
|
||||||
|
const token = this.tokens.find((t) => t.secret === secret);
|
||||||
|
return Promise.resolve(token && new Date(token.expiresAt) > new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(): Promise<number> {
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
|
||||||
|
async delete(secret: string): Promise<void> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllActive(): Promise<PublicSignupTokenSchema[]> {
|
||||||
|
return Promise.resolve(this.tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
newToken: IPublicSignupTokenCreate,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
return this.insert(newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(
|
||||||
|
newToken: IPublicSignupTokenCreate,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
const token = {
|
||||||
|
secret: 'some-secret',
|
||||||
|
expiresAt: newToken.expiresAt.toISOString(),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
users: [],
|
||||||
|
name: newToken.name,
|
||||||
|
role: {
|
||||||
|
name: 'Viewer',
|
||||||
|
type: '',
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
createdBy: null,
|
||||||
|
};
|
||||||
|
this.tokens.push(token);
|
||||||
|
return Promise.resolve(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setExpiry(
|
||||||
|
secret: string,
|
||||||
|
expiresAt: Date,
|
||||||
|
): Promise<PublicSignupTokenSchema> {
|
||||||
|
const token = await this.get(secret);
|
||||||
|
token.expiresAt = expiresAt.toISOString();
|
||||||
|
return Promise.resolve(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {}
|
||||||
|
|
||||||
|
async exists(key: string): Promise<boolean> {
|
||||||
|
return this.tokens.some((t) => t.secret === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
|
||||||
|
async getAll(query?: Object): Promise<PublicSignupTokenSchema[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
}
|
20
src/test/fixtures/fake-role-store.ts
vendored
20
src/test/fixtures/fake-role-store.ts
vendored
@ -8,6 +8,8 @@ import {
|
|||||||
} from 'lib/types/stores/role-store';
|
} from 'lib/types/stores/role-store';
|
||||||
|
|
||||||
export default class FakeRoleStore implements IRoleStore {
|
export default class FakeRoleStore implements IRoleStore {
|
||||||
|
roles: ICustomRole[] = [];
|
||||||
|
|
||||||
getGroupRolesForProject(projectId: string): Promise<IRole[]> {
|
getGroupRolesForProject(projectId: string): Promise<IRole[]> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@ -16,12 +18,14 @@ export default class FakeRoleStore implements IRoleStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(): Promise<ICustomRole[]> {
|
async getAll(): Promise<ICustomRole[]> {
|
||||||
throw new Error('Method not implemented.');
|
return this.roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(role: ICustomRoleInsert): Promise<ICustomRole> {
|
async create(role: ICustomRoleInsert): Promise<ICustomRole> {
|
||||||
throw new Error('Method not implemented.');
|
const roleCreated = { ...role, id: 1, type: 'some-type' };
|
||||||
|
this.roles.push(roleCreated);
|
||||||
|
return Promise.resolve(roleCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(role: ICustomRoleUpdate): Promise<ICustomRole> {
|
update(role: ICustomRoleUpdate): Promise<ICustomRole> {
|
||||||
@ -36,8 +40,8 @@ export default class FakeRoleStore implements IRoleStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleByName(name: string): Promise<IRole> {
|
async getRoleByName(name: string): Promise<IRole> {
|
||||||
throw new Error('Method not implemented.');
|
return this.roles.find((r) => (r.name = name));
|
||||||
}
|
}
|
||||||
|
|
||||||
getRolesForProject(projectId: string): Promise<IRole[]> {
|
getRolesForProject(projectId: string): Promise<IRole[]> {
|
||||||
@ -52,8 +56,8 @@ export default class FakeRoleStore implements IRoleStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
getRootRoles(): Promise<IRole[]> {
|
async getRootRoles(): Promise<IRole[]> {
|
||||||
throw new Error('Method not implemented.');
|
return this.roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
||||||
|
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 FakePublicSignupStore from './fake-public-signup-store';
|
||||||
|
|
||||||
const createStores: () => IUnleashStores = () => {
|
const createStores: () => IUnleashStores = () => {
|
||||||
const db = {
|
const db = {
|
||||||
@ -65,6 +66,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
roleStore: new FakeRoleStore(),
|
roleStore: new FakeRoleStore(),
|
||||||
segmentStore: new FakeSegmentStore(),
|
segmentStore: new FakeSegmentStore(),
|
||||||
groupStore: new FakeGroupStore(),
|
groupStore: new FakeGroupStore(),
|
||||||
|
publicSignupTokenStore: new FakePublicSignupStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user