1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

refactor: add OpenAPI schema to simple-password-provider controller (#1734)

* refactor: add OpenAPI schema to simple-password-provider controller

* finish implementation after merge

* refactor: address PR comments
This commit is contained in:
Nuno Góis 2022-06-23 08:40:25 +01:00 committed by GitHub
parent 89d25c8634
commit a792594e98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 43 deletions

View File

@ -23,6 +23,7 @@ import { healthCheckSchema } from './spec/health-check-schema';
import { healthOverviewSchema } from './spec/health-overview-schema'; import { healthOverviewSchema } from './spec/health-overview-schema';
import { healthReportSchema } from './spec/health-report-schema'; import { healthReportSchema } from './spec/health-report-schema';
import { legalValueSchema } from './spec/legal-value-schema'; import { legalValueSchema } from './spec/legal-value-schema';
import { loginSchema } from './spec/login-schema';
import { idSchema } from './spec/id-schema'; import { idSchema } from './spec/id-schema';
import { mapValues } from '../util/map-values'; import { mapValues } from '../util/map-values';
import { nameSchema } from './spec/name-schema'; import { nameSchema } from './spec/name-schema';
@ -116,6 +117,7 @@ export const schemas = {
healthOverviewSchema, healthOverviewSchema,
healthReportSchema, healthReportSchema,
legalValueSchema, legalValueSchema,
loginSchema,
nameSchema, nameSchema,
idSchema, idSchema,
meSchema, meSchema,
@ -172,16 +174,12 @@ export interface JsonSchemaProps {
components: object; components: object;
} }
interface ApiOperation<Tag = 'client' | 'admin' | 'other'> export interface ApiOperation<Tag = 'admin' | 'client' | 'auth' | 'other'>
extends Omit<OpenAPIV3.OperationObject, 'tags'> { extends Omit<OpenAPIV3.OperationObject, 'tags'> {
operationId: string; operationId: string;
tags: [Tag]; tags: [Tag];
} }
export type AdminApiOperation = ApiOperation<'admin'>;
export type ClientApiOperation = ApiOperation<'client'>;
export type OtherApiOperation = ApiOperation<'other'>;
export const createRequestSchema = ( export const createRequestSchema = (
schemaName: string, schemaName: string,
): OpenAPIV3.RequestBodyObject => { ): OpenAPIV3.RequestBodyObject => {

View File

@ -0,0 +1,19 @@
import { FromSchema } from 'json-schema-to-ts';
export const loginSchema = {
$id: '#/components/schemas/loginSchema',
type: 'object',
additionalProperties: false,
required: ['username', 'password'],
properties: {
username: {
type: 'string',
},
password: {
type: 'string',
},
},
components: {},
} as const;
export type LoginSchema = FromSchema<typeof loginSchema>;

View File

@ -55,7 +55,7 @@ class ResetPasswordController extends Controller {
permission: NONE, permission: NONE,
middleware: [ middleware: [
openApiService.validPath({ openApiService.validPath({
tags: ['other'], tags: ['auth'],
operationId: 'validateToken', operationId: 'validateToken',
responses: { 200: createResponseSchema('tokenUserSchema') }, responses: { 200: createResponseSchema('tokenUserSchema') },
}), }),

View File

@ -1,18 +1,24 @@
import request from 'supertest'; import request from 'supertest';
import express from 'express'; import express from 'express';
import User from '../../types/user'; import User from '../../types/user';
import PasswordProvider from './simple-password-provider'; import { SimplePasswordProvider } from './simple-password-provider';
import PasswordMismatchError from '../../error/password-mismatch'; import PasswordMismatchError from '../../error/password-mismatch';
import getLogger from '../../../test/fixtures/no-logger'; import { createTestConfig } from '../../../test/config/test-config';
import { OpenApiService } from '../../services/openapi-service';
test('Should require password', async () => { test('Should require password', async () => {
const config = createTestConfig();
const openApiService = new OpenApiService(config);
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
const userService = () => {}; const userService = () => {};
// @ts-ignore
const ctr = new PasswordProvider({ getLogger }, { userService });
//@ts-ignore const ctr = new SimplePasswordProvider(config, {
// @ts-expect-error
userService,
openApiService,
});
app.use('/auth/simple', ctr.router); app.use('/auth/simple', ctr.router);
const res = await request(app) const res = await request(app)
@ -23,6 +29,8 @@ test('Should require password', async () => {
}); });
test('Should login user', async () => { test('Should login user', async () => {
const config = createTestConfig();
const openApiService = new OpenApiService(config);
const username = 'ola'; const username = 'ola';
const password = 'simplepass'; const password = 'simplepass';
const user = new User({ id: 123, username }); const user = new User({ id: 123, username });
@ -30,10 +38,11 @@ test('Should login user', async () => {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
app.use((req, res, next) => { app.use((req, res, next) => {
//@ts-ignore // @ts-expect-error
req.session = {}; req.session = {};
next(); next();
}); });
const userService = { const userService = {
loginUser: (u, p) => { loginUser: (u, p) => {
if (u === username && p === password) { if (u === username && p === password) {
@ -42,10 +51,13 @@ test('Should login user', async () => {
throw new Error('Wrong password'); throw new Error('Wrong password');
}, },
}; };
// @ts-ignore
const ctr = new PasswordProvider({ getLogger }, { userService });
//@ts-ignore const ctr = new SimplePasswordProvider(config, {
// @ts-expect-error
userService,
openApiService,
});
app.use('/auth/simple', ctr.router); app.use('/auth/simple', ctr.router);
const res = await request(app) const res = await request(app)
@ -57,6 +69,8 @@ test('Should login user', async () => {
}); });
test('Should not login user with wrong password', async () => { test('Should not login user with wrong password', async () => {
const config = createTestConfig();
const openApiService = new OpenApiService(config);
const username = 'ola'; const username = 'ola';
const password = 'simplepass'; const password = 'simplepass';
const user = new User({ id: 133, username }); const user = new User({ id: 133, username });
@ -64,10 +78,11 @@ test('Should not login user with wrong password', async () => {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
app.use((req, res, next) => { app.use((req, res, next) => {
//@ts-ignore // @ts-expect-error
req.session = {}; req.session = {};
next(); next();
}); });
const userService = { const userService = {
loginUser: (u, p) => { loginUser: (u, p) => {
if (u === username && p === password) { if (u === username && p === password) {
@ -76,10 +91,13 @@ test('Should not login user with wrong password', async () => {
throw new PasswordMismatchError(); throw new PasswordMismatchError();
}, },
}; };
// @ts-ignore
const ctr = new PasswordProvider({ getLogger }, { userService });
//@ts-ignore const ctr = new SimplePasswordProvider(config, {
// @ts-expect-error
userService,
openApiService,
});
app.use('/auth/simple', ctr.router); app.use('/auth/simple', ctr.router);
const res = await request(app) const res = await request(app)

View File

@ -1,4 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { OpenApiService } from '../../services/openapi-service';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { IUnleashConfig } from '../../server-impl'; import { IUnleashConfig } from '../../server-impl';
import UserService from '../../services/user-service'; import UserService from '../../services/user-service';
@ -6,37 +7,61 @@ import { IUnleashServices } from '../../types';
import { NONE } from '../../types/permissions'; import { NONE } from '../../types/permissions';
import Controller from '../controller'; import Controller from '../controller';
import { IAuthRequest } from '../unleash-types'; import { IAuthRequest } from '../unleash-types';
import { createRequestSchema, createResponseSchema } from '../../openapi';
import { userSchema, UserSchema } from '../../openapi/spec/user-schema';
import { LoginSchema } from '../../openapi/spec/login-schema';
import { serializeDates } from '../../types/serialize-dates';
class PasswordProvider extends Controller { export class SimplePasswordProvider extends Controller {
private userService: UserService;
private logger: Logger; private logger: Logger;
private openApiService: OpenApiService;
private userService: UserService;
constructor( constructor(
config: IUnleashConfig, config: IUnleashConfig,
{ userService }: Pick<IUnleashServices, 'userService'>, {
userService,
openApiService,
}: Pick<IUnleashServices, 'userService' | 'openApiService'>,
) { ) {
super(config); super(config);
this.logger = config.getLogger('/auth/password-provider.js'); this.logger = config.getLogger('/auth/password-provider.js');
this.openApiService = openApiService;
this.userService = userService; this.userService = userService;
this.post('/login', this.login, NONE); this.route({
} method: 'post',
path: '/login',
async login(req: IAuthRequest, res: Response): Promise<void> { handler: this.login,
const { username, password } = req.body; permission: NONE,
middleware: [
if (!username || !password) { openApiService.validPath({
res.status(400).json({ tags: ['auth'],
message: 'You must provide username and password', operationId: 'login',
requestBody: createRequestSchema('loginSchema'),
responses: {
200: createResponseSchema('userSchema'),
},
}),
],
}); });
return;
} }
async login(
req: IAuthRequest<void, void, LoginSchema>,
res: Response<UserSchema>,
): Promise<void> {
const { username, password } = req.body;
const user = await this.userService.loginUser(username, password); const user = await this.userService.loginUser(username, password);
req.session.user = user; req.session.user = user;
res.status(200).json(user); this.openApiService.respondWithValidation(
200,
res,
userSchema.$id,
serializeDates(user),
);
} }
} }
export default PasswordProvider;

View File

@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { BackstageController } from './backstage'; import { BackstageController } from './backstage';
import ResetPasswordController from './auth/reset-password-controller'; import ResetPasswordController from './auth/reset-password-controller';
import SimplePasswordProvider from './auth/simple-password-provider'; import { SimplePasswordProvider } from './auth/simple-password-provider';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
import { IUnleashServices } from '../types/services'; import { IUnleashServices } from '../types/services';
import { api } from './api-def'; import { api } from './api-def';

View File

@ -2,9 +2,7 @@ import openapi, { IExpressOpenApi } from '@unleash/express-openapi';
import { Express, RequestHandler, Response } from 'express'; import { Express, RequestHandler, Response } from 'express';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
import { import {
AdminApiOperation, ApiOperation,
ClientApiOperation,
OtherApiOperation,
createOpenApiSchema, createOpenApiSchema,
JsonSchemaProps, JsonSchemaProps,
removeJsonSchemaProps, removeJsonSchemaProps,
@ -31,9 +29,7 @@ export class OpenApiService {
); );
} }
validPath( validPath(op: ApiOperation): RequestHandler {
op: AdminApiOperation | ClientApiOperation | OtherApiOperation,
): RequestHandler {
return this.api.validPath(op); return this.api.validPath(op);
} }

View File

@ -1041,6 +1041,22 @@ Object {
], ],
"type": "object", "type": "object",
}, },
"loginSchema": Object {
"additionalProperties": false,
"properties": Object {
"password": Object {
"type": "string",
},
"username": Object {
"type": "string",
},
},
"required": Array [
"username",
"password",
],
"type": "object",
},
"meSchema": Object { "meSchema": Object {
"additionalProperties": false, "additionalProperties": false,
"properties": Object { "properties": Object {
@ -4950,7 +4966,7 @@ Object {
}, },
}, },
"tags": Array [ "tags": Array [
"other", "auth",
], ],
}, },
}, },
@ -4978,6 +4994,37 @@ Object {
], ],
}, },
}, },
"/auth/simple/login": Object {
"post": Object {
"operationId": "login",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/loginSchema",
},
},
},
"description": "loginSchema",
"required": true,
},
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/userSchema",
},
},
},
"description": "userSchema",
},
},
"tags": Array [
"auth",
],
},
},
"/health": Object { "/health": Object {
"get": Object { "get": Object {
"operationId": "getHealth", "operationId": "getHealth",