diff --git a/.github/workflows/build_doc_prs.yaml b/.github/workflows/build_doc_prs.yaml new file mode 100644 index 0000000000..c332f67003 --- /dev/null +++ b/.github/workflows/build_doc_prs.yaml @@ -0,0 +1,17 @@ +name: PR -> Build Docs + +on: + pull_request: + paths: + - website/** + +jobs: + validate: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: setup git config + run: | + # Build the site + cd website && yarn && yarn build diff --git a/package.json b/package.json index 2fa0cbcb71..c794de6dad 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "del-cli": "4.0.1", "eslint": "8.3.0", "eslint-config-airbnb-base": "15.0.0", - "eslint-config-airbnb-typescript": "15.0.0", + "eslint-config-airbnb-typescript": "16.0.0", "eslint-config-prettier": "8.3.0", "eslint-plugin-import": "2.25.3", "eslint-plugin-prettier": "4.0.0", diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index a7d0c07d04..fbe9cec78f 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -29,6 +29,7 @@ import EnvironmentStore from './environment-store'; import FeatureTagStore from './feature-tag-store'; import { FeatureEnvironmentStore } from './feature-environment-store'; import { ClientMetricsStoreV2 } from './client-metrics-store-v2'; +import UserSplashStore from './user-splash-store'; export const createStores = ( config: IUnleashConfig, @@ -85,6 +86,7 @@ export const createStores = ( eventBus, getLogger, ), + userSplashStore: new UserSplashStore(db, eventBus, getLogger), }; }; diff --git a/src/lib/db/user-splash-store.ts b/src/lib/db/user-splash-store.ts new file mode 100644 index 0000000000..6b867ee4d7 --- /dev/null +++ b/src/lib/db/user-splash-store.ts @@ -0,0 +1,105 @@ +import { Knex } from 'knex'; +import { EventEmitter } from 'events'; +import { LogProvider, Logger } from '../logger'; +import { + IUserSplash, + IUserSplashKey, + IUserSplashStore, +} from '../types/stores/user-splash-store'; + +const COLUMNS = ['user_id', 'splash_id', 'seen']; +const TABLE = 'user_splash'; + +interface IUserSplashTable { + seen?: boolean; + splash_id: string; + user_id: number; +} + +const fieldToRow = (fields: IUserSplash): IUserSplashTable => ({ + seen: fields.seen, + splash_id: fields.splashId, + user_id: fields.userId, +}); + +const rowToField = (row: IUserSplashTable): IUserSplash => ({ + seen: row.seen, + splashId: row.splash_id, + userId: row.user_id, +}); + +export default class UserSplashStore implements IUserSplashStore { + private db: Knex; + + private logger: Logger; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('user-splash-store.ts'); + } + + async getAllUserSplashs(userId: number): Promise { + const userSplash = await this.db + .table(TABLE) + .select() + .where({ user_id: userId }); + + return userSplash.map(rowToField); + } + + async getSplash(userId: number, splashId: string): Promise { + const userSplash = await this.db + .table(TABLE) + .select() + .where({ user_id: userId, splash_id: splashId }) + .first(); + + return rowToField(userSplash); + } + + async updateSplash(splash: IUserSplash): Promise { + const insertedSplash = await this.db + .table(TABLE) + .insert(fieldToRow(splash)) + .onConflict(['user_id', 'splash_id']) + .merge() + .returning(COLUMNS); + + return rowToField(insertedSplash[0]); + } + + async delete({ userId, splashId }: IUserSplashKey): Promise { + await this.db(TABLE) + .where({ user_id: userId, splash_id: splashId }) + .del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists({ userId, splashId }: IUserSplashKey): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE user_id = ? AND splash_id = ?) AS present`, + [userId, splashId], + ); + const { present } = result.rows[0]; + return present; + } + + async get({ userId, splashId }: IUserSplashKey): Promise { + return this.getSplash(userId, splashId); + } + + async getAll(): Promise { + const userSplashs = await this.db + .table(TABLE) + .select(); + + return userSplashs.map(rowToField); + } +} + +module.exports = UserSplashStore; diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 3e0edf6114..f1aa75c7fa 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -21,6 +21,7 @@ import ApiTokenController from './api-token-controller'; import UserAdminController from './user-admin'; import EmailController from './email'; import UserFeedbackController from './user-feedback-controller'; +import UserSplashController from './user-splash-controller'; import ProjectApi from './project'; import { EnvironmentsController } from './environments-controller'; @@ -92,6 +93,10 @@ class AdminApi extends Controller { '/environments', new EnvironmentsController(config, services).router, ); + this.app.use( + '/splash', + new UserSplashController(config, services).router, + ); } index(req, res) { diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash-controller.ts new file mode 100644 index 0000000000..07f3b7a049 --- /dev/null +++ b/src/lib/routes/admin-api/user-splash-controller.ts @@ -0,0 +1,49 @@ +import { Response } from 'express'; + +import Controller from '../controller'; +import { Logger } from '../../logger'; +import { IUnleashConfig } from '../../types/option'; +import { IUnleashServices } from '../../types/services'; +import UserSplashService from '../../services/user-splash-service'; +import { IAuthRequest } from '../unleash-types'; + +interface ISplashBody { + seen: boolean; + splashId: string; +} + +class UserSplashController extends Controller { + private logger: Logger; + + private userSplashService: UserSplashService; + + constructor( + config: IUnleashConfig, + { userSplashService }: Pick, + ) { + super(config); + this.logger = config.getLogger('splash-controller.ts'); + this.userSplashService = userSplashService; + + this.post('/:id', this.updateSplashSettings); + } + + private async updateSplashSettings( + req: IAuthRequest, + res: Response, + ): Promise { + const { user } = req; + const { id } = req.params; + + const splash = { + splashId: id, + userId: user.id, + seen: true, + }; + const updated = await this.userSplashService.updateSplash(splash); + res.json(updated); + } +} + +module.exports = UserSplashController; +export default UserSplashController; diff --git a/src/lib/routes/admin-api/user.ts b/src/lib/routes/admin-api/user.ts index 6b5299311f..a0449d1b8d 100644 --- a/src/lib/routes/admin-api/user.ts +++ b/src/lib/routes/admin-api/user.ts @@ -7,6 +7,7 @@ import { IUnleashServices } from '../../types/services'; import UserService from '../../services/user-service'; import SessionService from '../../services/session-service'; import UserFeedbackService from '../../services/user-feedback-service'; +import UserSplashService from '../../services/user-splash-service'; interface IChangeUserRequest { password: string; @@ -22,6 +23,8 @@ class UserController extends Controller { private sessionService: SessionService; + private userSplashService: UserSplashService; + constructor( config: IUnleashConfig, { @@ -29,12 +32,14 @@ class UserController extends Controller { userService, sessionService, userFeedbackService, + userSplashService, }: Pick< IUnleashServices, | 'accessService' | 'userService' | 'sessionService' | 'userFeedbackService' + | 'userSplashService' >, ) { super(config); @@ -42,6 +47,7 @@ class UserController extends Controller { this.userService = userService; this.sessionService = sessionService; this.userFeedbackService = userFeedbackService; + this.userSplashService = userSplashService; this.get('/', this.getUser); this.post('/change-password', this.updateUserPass); @@ -57,11 +63,15 @@ class UserController extends Controller { const feedback = await this.userFeedbackService.getAllUserFeedback( user, ); + const splash = await this.userSplashService.getAllUserSplashs(user); // TODO: remove this line after we remove it from db. delete user.permissions; - return res.status(200).json({ user, permissions, feedback }).end(); + return res + .status(200) + .json({ user, permissions, feedback, splash }) + .end(); } async updateUserPass( diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 8e39e913cb..5d3ddc3e95 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -27,6 +27,7 @@ import FeatureToggleService from './feature-toggle-service'; import EnvironmentService from './environment-service'; import FeatureTagService from './feature-tag-service'; import ProjectHealthService from './project-health-service'; +import UserSplashService from './user-splash-service'; export const createServices = ( stores: IUnleashStores, @@ -72,6 +73,7 @@ export const createServices = ( accessService, featureToggleServiceV2, ); + const userSplashService = new UserSplashService(stores, config); return { accessService, @@ -100,6 +102,7 @@ export const createServices = ( userFeedbackService, featureTagService, projectHealthService, + userSplashService, }; }; diff --git a/src/lib/services/user-splash-service.ts b/src/lib/services/user-splash-service.ts new file mode 100644 index 0000000000..7d919c7dce --- /dev/null +++ b/src/lib/services/user-splash-service.ts @@ -0,0 +1,54 @@ +import { Logger } from '../logger'; +import { IUnleashStores } from '../types/stores'; +import { IUnleashConfig } from '../types/option'; +import User from '../types/user'; +import { + IUserSplash, + IUserSplashStore, +} from '../types/stores/user-splash-store'; + +export default class UserSplashService { + private userSplashStore: IUserSplashStore; + + private logger: Logger; + + constructor( + { userSplashStore }: Pick, + { getLogger }: Pick, + ) { + this.userSplashStore = userSplashStore; + this.logger = getLogger('services/user-splash-service.js'); + } + + async getAllUserSplashs(user: User): Promise { + if (user.isAPI) { + return []; + } + try { + const splashs = ( + await this.userSplashStore.getAllUserSplashs(user.id) + ).reduce( + (splashObject, splash) => ({ + ...splashObject, + [splash.splashId]: splash.seen, + }), + {}, + ); + return splashs; + } catch (err) { + this.logger.error(err); + + return {}; + } + } + + async getSplash(user_id: number, splash_id: string): Promise { + return this.userSplashStore.getSplash(user_id, splash_id); + } + + async updateSplash(splash: IUserSplash): Promise { + return this.userSplashStore.updateSplash(splash); + } +} + +module.exports = UserSplashService; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index b2528df9f8..52dab846bb 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -23,6 +23,7 @@ import EnvironmentService from '../services/environment-service'; import FeatureTagService from '../services/feature-tag-service'; import ProjectHealthService from '../services/project-health-service'; import ClientMetricsServiceV2 from '../services/client-metrics/client-metrics-service-v2'; +import UserSplashService from '../services/user-splash-service'; export interface IUnleashServices { accessService: AccessService; @@ -51,4 +52,5 @@ export interface IUnleashServices { userFeedbackService: UserFeedbackService; userService: UserService; versionService: VersionService; + userSplashService: UserSplashService; } diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index ff22f24b3f..cb92c0c44a 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -23,6 +23,7 @@ import { IFeatureStrategiesStore } from './stores/feature-strategies-store'; import { IEnvironmentStore } from './stores/environment-store'; import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store'; import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; +import { IUserSplashStore } from './stores/user-splash-store'; export interface IUnleashStores { accessStore: IAccessStore; @@ -50,4 +51,5 @@ export interface IUnleashStores { tagTypeStore: ITagTypeStore; userFeedbackStore: IUserFeedbackStore; userStore: IUserStore; + userSplashStore: IUserSplashStore; } diff --git a/src/lib/types/stores/user-splash-store.ts b/src/lib/types/stores/user-splash-store.ts new file mode 100644 index 0000000000..75938981ef --- /dev/null +++ b/src/lib/types/stores/user-splash-store.ts @@ -0,0 +1,18 @@ +import { Store } from './store'; + +export interface IUserSplash { + seen: boolean; + splashId: string; + userId: number; +} + +export interface IUserSplashKey { + userId: number; + splashId: string; +} + +export interface IUserSplashStore extends Store { + getAllUserSplashs(userId: number): Promise; + getSplash(userId: number, splashId: string): Promise; + updateSplash(splash: IUserSplash): Promise; +} diff --git a/src/migrations/20211108130333-create-user-splash-table.js b/src/migrations/20211108130333-create-user-splash-table.js new file mode 100644 index 0000000000..38ea54e3d9 --- /dev/null +++ b/src/migrations/20211108130333-create-user-splash-table.js @@ -0,0 +1,25 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + CREATE TABLE IF NOT EXISTS user_splash + (user_id INTEGER NOT NULL references users (id) ON DELETE CASCADE, + splash_id TEXT, + seen BOOLEAN NOT NULL DEFAULT false, + PRIMARY KEY (user_id, splash_id)); + CREATE INDEX user_splash_user_id_idx ON user_splash (user_id); + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DROP INDEX user_splash_user_id_idx; + DROP TABLE user_splash; +`, + cb, + ); +}; diff --git a/src/migrations/20211109103930-add-splash-entry-for-users.js b/src/migrations/20211109103930-add-splash-entry-for-users.js new file mode 100644 index 0000000000..65d70586ea --- /dev/null +++ b/src/migrations/20211109103930-add-splash-entry-for-users.js @@ -0,0 +1,10 @@ +exports.up = function (db, cb) { + db.runSql( + `INSERT INTO user_splash(splash_id, user_id, seen) SELECT 'environment', u.id, false FROM users u`, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql('DELETE FROM user_splash', cb); +}; diff --git a/src/test/e2e/api/admin/splash.e2e.test.ts b/src/test/e2e/api/admin/splash.e2e.test.ts new file mode 100644 index 0000000000..61d0c6db26 --- /dev/null +++ b/src/test/e2e/api/admin/splash.e2e.test.ts @@ -0,0 +1,68 @@ +import { Application, NextFunction, Request, Response } from 'express'; +import { setupAppWithCustomAuth } from '../../helpers/test-helper'; +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; +import { IUnleashConfig } from '../../../../lib/types/option'; +import { IUnleashServices } from '../../../../lib/types/services'; + +let stores; +let db; +let app; + +beforeAll(async () => { + db = await dbInit('splash_api_serial', getLogger); + stores = db.stores; + + const email = 'custom-user@mail.com'; + + const preHook = ( + application: Application, + config: IUnleashConfig, + { userService }: IUnleashServices, + ) => { + application.use( + '/api/admin/', + async (req: Request, res: Response, next: NextFunction) => { + // @ts-ignore + req.user = await userService.loginUserWithoutPassword( + email, + true, + ); + next(); + }, + ); + }; + + app = await setupAppWithCustomAuth(stores, preHook); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('it updates splash for user', async () => { + expect.assertions(1); + + return app.request + .post('/api/admin/splash/environment') + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.seen).toBe(true); + }); +}); + +test('it retrieves splash for user', async () => { + expect.assertions(1); + + return app.request + .get('/api/admin/user') + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.splash).toStrictEqual({ environment: true }); + }); +}); diff --git a/src/test/e2e/stores/user-splash-store.e2e.test.ts b/src/test/e2e/stores/user-splash-store.e2e.test.ts new file mode 100644 index 0000000000..73050f2c0b --- /dev/null +++ b/src/test/e2e/stores/user-splash-store.e2e.test.ts @@ -0,0 +1,82 @@ +import { IUserSplashStore } from 'lib/types/stores/user-splash-store'; +import { IUserStore } from 'lib/types/stores/user-store'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; + +let stores; +let db; +let userSplashStore: IUserSplashStore; +let userStore: IUserStore; +let currentUser; + +beforeAll(async () => { + db = await dbInit('user_splash_store', getLogger); + stores = db.stores; + userSplashStore = stores.userSplashStore; + userStore = stores.userStore; + currentUser = await userStore.upsert({ email: 'me.feedback@mail.com' }); +}); + +afterAll(async () => { + await db.destroy(); +}); + +afterEach(async () => { + await userSplashStore.deleteAll(); +}); + +test('should create userSplash', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id', + userId: currentUser.id, + seen: false, + }); + const userSplashs = await userSplashStore.getAllUserSplashs(currentUser.id); + expect(userSplashs).toHaveLength(1); + expect(userSplashs[0].splashId).toBe('some-id'); +}); + +test('should get userSplash', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id', + userId: currentUser.id, + seen: false, + }); + const userSplash = await userSplashStore.getSplash( + currentUser.id, + 'some-id', + ); + expect(userSplash.splashId).toBe('some-id'); +}); + +test('should exists', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id-3', + userId: currentUser.id, + seen: false, + }); + const exists = await userSplashStore.exists({ + userId: currentUser.id, + splashId: 'some-id-3', + }); + expect(exists).toBe(true); +}); + +test('should not exists', async () => { + const exists = await userSplashStore.exists({ + userId: currentUser.id, + splashId: 'some-id-not-here', + }); + expect(exists).toBe(false); +}); + +test('should get all userSplashs', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id-2', + userId: currentUser.id, + seen: false, + }); + const userSplashs = await userSplashStore.getAll(); + expect(userSplashs).toHaveLength(1); + expect(userSplashs[0].splashId).toBe('some-id-2'); +}); diff --git a/src/test/fixtures/fake-user-splash-store.ts b/src/test/fixtures/fake-user-splash-store.ts new file mode 100644 index 0000000000..5b3c6e8331 --- /dev/null +++ b/src/test/fixtures/fake-user-splash-store.ts @@ -0,0 +1,50 @@ +import { + IUserSplashKey, + IUserSplash, + IUserSplashStore, +} from '../../lib/types/stores/user-splash-store'; + +export default class FakeUserSplashStore implements IUserSplashStore { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllUserSplashs(userId: number): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getSplash(userId: number, splashId: string): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateSplash(splash: IUserSplash): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + exists(key: IUserSplashKey): Promise { + return Promise.resolve(false); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get(key: IUserSplashKey): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAll(): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + delete(key: IUserSplashKey): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deleteAll(): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + destroy(): void {} +} diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 5e36e35ff0..6b6bf25d04 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -24,6 +24,7 @@ import FakeFeatureTypeStore from './fake-feature-type-store'; import FakeResetTokenStore from './fake-reset-token-store'; import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store'; import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2'; +import FakeUserSplashStore from './fake-user-splash-store'; const createStores: () => IUnleashStores = () => { const db = { @@ -59,6 +60,7 @@ const createStores: () => IUnleashStores = () => { featureTypeStore: new FakeFeatureTypeStore(), resetTokenStore: new FakeResetTokenStore(), sessionStore: new FakeSessionStore(), + userSplashStore: new FakeUserSplashStore(), }; }; diff --git a/website/docs/deploy/getting-started.md b/website/docs/deploy/getting-started.md index f369b547b8..6a13a1e2ee 100644 --- a/website/docs/deploy/getting-started.md +++ b/website/docs/deploy/getting-started.md @@ -57,11 +57,13 @@ docker run -p 4242:4242 \ -e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ -e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ -e DATABASE_SSL=false \ - --network unleash unleashorg/unleash-server + --network unleash --pull=always unleashorg/unleash-server ``` ### Option 2 - use Docker-compose {#option-two---use-docker-compose} + **Steps:** + 1. Clone the [unleash-docker](https://github.com/Unleash/unleash-docker) repository. 2. Run `docker-compose build` in repository root folder. 3. Run `docker-compose up` in repository root folder. @@ -95,7 +97,7 @@ docker run -p 4242:4242 \ port: 4242, }, }) - .then(unleash => { + .then((unleash) => { console.log( `Unleash started on http://localhost:${unleash.app.get('port')}`, ); diff --git a/website/docs/user_guide/quickstart.md b/website/docs/user_guide/quickstart.md index 9fdab31479..a975312a3b 100644 --- a/website/docs/user_guide/quickstart.md +++ b/website/docs/user_guide/quickstart.md @@ -198,27 +198,42 @@ unleash.on('synchronized', () => { ## I want to run Unleash locally -### Run Unleash with Docker +### Run Unleash with Docker {#run-unleash-with-docker} The easiest way to run unleash locally is using [docker](https://www.docker.com/). +:::tip + +Each container that runs in your local Docker instance must have a unique name. If you've run these commands before, you can either start the containers again (`docker start ${CONTAINER_NAME}`) or remove them (`docker rm ${CONTAINER_NAME}`) and run the commands again. + +::: + 1. Create a network by running `docker network create unleash` 2. Start a postgres database: ```sh -docker run -e POSTGRES_PASSWORD=some_password \ - -e POSTGRES_USER=unleash_user -e POSTGRES_DB=unleash \ - --network unleash --name postgres postgres +docker run \ + -e POSTGRES_USER=unleash_user \ + -e POSTGRES_PASSWORD=some_password \ + -e POSTGRES_DB=unleash \ + --network unleash \ + --name postgres \ + postgres ``` 3. Start Unleash via docker: ```sh -docker run -p 4242:4242 \ - -e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ - -e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ +docker run \ + -p 4242:4242 \ + -e DATABASE_HOST=postgres \ + -e DATABASE_NAME=unleash \ + -e DATABASE_USERNAME=unleash_user \ + -e DATABASE_PASSWORD=some_password \ -e DATABASE_SSL=false \ - --network unleash unleashorg/unleash-server + --network unleash \ + --name unleash \ + --pull=always unleashorg/unleash-server ``` [Click here to see all options to get started locally.](deploy/getting-started.md) @@ -232,6 +247,58 @@ username: admin password: unleash4all ``` +### Run Unleash and the Unleash proxy with Docker + +Follow steps outlined in the [Run Unleash with Docker](#run-unleash-with-docker) section to get the Unleash instance up and running. Once you have done that you need to first get an API key from your Unleash instance and then use that API key when starting the Unleash proxy. + +1. Get an API key. + + To get an API key, access your Unleash instance in a web browser. First, navigate to the API access screen. + + [![The Unleash UI showing a dropdown menu under the Configure menu +entry. The dropdown menu's API Access option is highlighted and +you're told to navigate there.]](/img/api_access_navigation.png 'Navigate to the API access page.') + + Next, create an API key with these details + + - **name:** proxy-key (this can be whatever you want) + - **token type:** client + - **project:** all + - **environment:** select your preferred environment (this option is only available in Unleash 4.3 and later) + + Copy the API key to your clipboard. You'll need it in the next step. + + :::note + + Depending on whether you have the environments feature enabled or not, the API key will look a little different. If you don't have environments enabled, it'll just be a 64 character long hexadecimal string (for instance `be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`). If you do have environments enabled, the key will be prefixed with the project and the environment that the key is valid for. It'll use the format `:.`, e.g. `demo-app:production.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`. + + Regardless of which format your string uses, do not modify it. + + ::: + +2. Start the Unleash proxy + + Start a container with the Unleash proxy by running the following command. Replace `${API_KEY}` with the key you created in the following step. + + ```sh + docker run \ + -e UNLEASH_PROXY_SECRETS=some-secret \ + -e UNLEASH_URL='http://unleash:4242/api/' \ + -e UNLEASH_API_TOKEN='${API_KEY}' \ + -p 3000:3000 \ + --network unleash \ + --name unleash-proxy \ + --pull=always unleashorg/unleash-proxy + ``` + +3. Test the proxy + + To make sure the proxy is running successfully, you can test it by running the following command: + + ```curl + curl http://localhost:3000/proxy -H "Authorization: some-secret" + ``` + ### Create your first toggle In order to create a toggle through the UI, [you can follow this guide](create-feature-toggle.md). Once you have created your feature toggle, you are ready to connect your application using an SDK. diff --git a/website/static/img/api_access_navigation.png b/website/static/img/api_access_navigation.png new file mode 100644 index 0000000000..9f83317ebb Binary files /dev/null and b/website/static/img/api_access_navigation.png differ diff --git a/yarn.lock b/yarn.lock index 31430e8333..5c7323f75f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2335,29 +2335,6 @@ errorhandler@^1.5.1: accepts "~1.3.7" escape-html "~1.0.3" -es-abstract@^1.18.2: - version "1.18.5" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz" - integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.3" - is-string "^1.0.6" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -2466,7 +2443,7 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@15.0.0: +eslint-config-airbnb-base@15.0.0, eslint-config-airbnb-base@^15.0.0: version "15.0.0" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== @@ -2476,21 +2453,12 @@ eslint-config-airbnb-base@15.0.0: object.entries "^1.1.5" semver "^6.3.0" -eslint-config-airbnb-base@^14.2.1: - version "14.2.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" - integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== +eslint-config-airbnb-typescript@16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-16.0.0.tgz#75007e27d5a7fb75530721f48197817c1d2ad4d1" + integrity sha512-qDOyD0YYZo5Us1YvOnWig2Ly/+IlQKmMZpnqKnJgVtHdK8SkjaSyVBHKbD41dEaQxk8vRVGBC94PuR2ceSwbLQ== dependencies: - confusing-browser-globals "^1.0.10" - object.assign "^4.1.2" - object.entries "^1.1.2" - -eslint-config-airbnb-typescript@15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-15.0.0.tgz#c88007b3cca5dd0f47125420ca5e8f6efac418fd" - integrity sha512-DTWGwqytbTnB8kSKtmkrGkRf3xwTs2l15shSH0w/3Img47AQwCCrIA/ON/Uj0XXBxP31LHyEItPXeuH3mqCNLA== - dependencies: - eslint-config-airbnb-base "^14.2.1" + eslint-config-airbnb-base "^15.0.0" eslint-config-prettier@8.3.0: version "8.3.0" @@ -3635,7 +3603,7 @@ is-buffer@^1.1.5: resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.3, is-callable@^1.2.4: +is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== @@ -3824,7 +3792,7 @@ is-promise@^2.2.2: resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-regex@^1.1.3, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -3854,7 +3822,7 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5, is-string@^1.0.6, is-string@^1.0.7: +is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== @@ -5274,15 +5242,6 @@ object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" -object.entries@^1.1.2: - version "1.1.4" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz" - integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.2" - object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861"