mirror of
https://github.com/ferdikoomen/openapi-typescript-codegen.git
synced 2025-12-08 20:16:21 +00:00
- Fixed #411
This commit is contained in:
parent
bf30cbfb85
commit
fe6a3be63e
@ -1,15 +1,24 @@
|
||||
{{>header}}
|
||||
|
||||
type Resolver<T> = () => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
interface Config {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
TOKEN: string | (() => Promise<string>);
|
||||
TOKEN?: string | Resolver<string>;
|
||||
USERNAME?: string | Resolver<string>;
|
||||
PASSWORD?: string | Resolver<string>;
|
||||
HEADERS?: Headers | Resolver<Headers>;
|
||||
}
|
||||
|
||||
export const OpenAPI: Config = {
|
||||
BASE: '{{{server}}}',
|
||||
VERSION: '{{{version}}}',
|
||||
WITH_CREDENTIALS: false,
|
||||
TOKEN: '',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
};
|
||||
|
||||
@ -1,14 +1,23 @@
|
||||
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
...OpenAPI.HEADERS,
|
||||
...options.headers,
|
||||
});
|
||||
|
||||
const token = await getToken();
|
||||
if (isDefined(token) && token !== '') {
|
||||
const token = await resolve(OpenAPI.TOKEN);
|
||||
const username = await resolve(OpenAPI.USERNAME);
|
||||
const password = await resolve(OpenAPI.PASSWORD);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers.append('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = btoa(`${username}:${password}`);
|
||||
headers.append('Authorization', `Basic ${credentials}`);
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (isBlob(options.body)) {
|
||||
headers.append('Content-Type', options.body.type || 'application/octet-stream');
|
||||
|
||||
@ -11,6 +11,9 @@ import { OpenAPI } from './OpenAPI';
|
||||
{{>functions/isString}}
|
||||
|
||||
|
||||
{{>functions/isStringWithValue}}
|
||||
|
||||
|
||||
{{>functions/isBlob}}
|
||||
|
||||
|
||||
@ -23,7 +26,7 @@ import { OpenAPI } from './OpenAPI';
|
||||
{{>functions/getFormData}}
|
||||
|
||||
|
||||
{{>functions/getToken}}
|
||||
{{>functions/resolve}}
|
||||
|
||||
|
||||
{{>fetch/getHeaders}}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
async function getToken(): Promise<string> {
|
||||
if (typeof OpenAPI.TOKEN === 'function') {
|
||||
return OpenAPI.TOKEN();
|
||||
}
|
||||
return OpenAPI.TOKEN;
|
||||
}
|
||||
3
src/templates/core/functions/isStringWithValue.hbs
Normal file
3
src/templates/core/functions/isStringWithValue.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
function isStringWithValue(value: any): value is string {
|
||||
return isString(value) && value !== '';
|
||||
}
|
||||
8
src/templates/core/functions/resolve.hbs
Normal file
8
src/templates/core/functions/resolve.hbs
Normal file
@ -0,0 +1,8 @@
|
||||
type Resolver<T> = () => Promise<T>;
|
||||
|
||||
async function resolve<T>(resolver?: T | Resolver<T>): Promise<T | undefined> {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)();
|
||||
}
|
||||
return resolver;
|
||||
}
|
||||
@ -1,14 +1,23 @@
|
||||
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
...OpenAPI.HEADERS,
|
||||
...options.headers,
|
||||
});
|
||||
|
||||
const token = await getToken();
|
||||
if (isDefined(token) && token !== '') {
|
||||
const token = await resolve(OpenAPI.TOKEN);
|
||||
const username = await resolve(OpenAPI.USERNAME);
|
||||
const password = await resolve(OpenAPI.PASSWORD);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers.append('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = Buffer.from(`${username}:${password}`).toString('base64');
|
||||
headers.append('Authorization', `Basic ${credentials}`);
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (isBinary(options.body)) {
|
||||
headers.append('Content-Type', 'application/octet-stream');
|
||||
|
||||
@ -15,6 +15,9 @@ import { OpenAPI } from './OpenAPI';
|
||||
{{>functions/isString}}
|
||||
|
||||
|
||||
{{>functions/isStringWithValue}}
|
||||
|
||||
|
||||
{{>functions/isBinary}}
|
||||
|
||||
|
||||
@ -27,7 +30,7 @@ import { OpenAPI } from './OpenAPI';
|
||||
{{>functions/getFormData}}
|
||||
|
||||
|
||||
{{>functions/getToken}}
|
||||
{{>functions/resolve}}
|
||||
|
||||
|
||||
{{>node/getHeaders}}
|
||||
|
||||
@ -1,14 +1,23 @@
|
||||
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
...OpenAPI.HEADERS,
|
||||
...options.headers,
|
||||
});
|
||||
|
||||
const token = await getToken();
|
||||
if (isDefined(token) && token !== '') {
|
||||
const token = await resolve(OpenAPI.TOKEN);
|
||||
const username = await resolve(OpenAPI.USERNAME);
|
||||
const password = await resolve(OpenAPI.PASSWORD);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers.append('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = btoa(`${username}:${password}`);
|
||||
headers.append('Authorization', `Basic ${credentials}`);
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (isBlob(options.body)) {
|
||||
headers.append('Content-Type', options.body.type || 'application/octet-stream');
|
||||
|
||||
@ -11,6 +11,9 @@ import { OpenAPI } from './OpenAPI';
|
||||
{{>functions/isString}}
|
||||
|
||||
|
||||
{{>functions/isStringWithValue}}
|
||||
|
||||
|
||||
{{>functions/isBlob}}
|
||||
|
||||
|
||||
@ -26,7 +29,7 @@ import { OpenAPI } from './OpenAPI';
|
||||
{{>functions/getFormData}}
|
||||
|
||||
|
||||
{{>functions/getToken}}
|
||||
{{>functions/resolve}}
|
||||
|
||||
|
||||
{{>fetch/getHeaders}}
|
||||
|
||||
@ -12,13 +12,14 @@ import fetchSendRequest from '../templates/core/fetch/sendRequest.hbs';
|
||||
import functionCatchErrors from '../templates/core/functions/catchErrors.hbs';
|
||||
import functionGetFormData from '../templates/core/functions/getFormData.hbs';
|
||||
import functionGetQueryString from '../templates/core/functions/getQueryString.hbs';
|
||||
import functionGetToken from '../templates/core/functions/getToken.hbs';
|
||||
import functionGetUrl from '../templates/core/functions/getUrl.hbs';
|
||||
import functionIsBinary from '../templates/core/functions/isBinary.hbs';
|
||||
import functionIsBlob from '../templates/core/functions/isBlob.hbs';
|
||||
import functionIsDefined from '../templates/core/functions/isDefined.hbs';
|
||||
import functionIsString from '../templates/core/functions/isString.hbs';
|
||||
import functionIsStringWithValue from '../templates/core/functions/isStringWithValue.hbs';
|
||||
import functionIsSuccess from '../templates/core/functions/isSuccess.hbs';
|
||||
import functionResolve from '../templates/core/functions/resolve.hbs';
|
||||
import nodeGetHeaders from '../templates/core/node/getHeaders.hbs';
|
||||
import nodeGetRequestBody from '../templates/core/node/getRequestBody.hbs';
|
||||
import nodeGetResponseBody from '../templates/core/node/getResponseBody.hbs';
|
||||
@ -132,14 +133,15 @@ export function registerHandlebarTemplates(): Templates {
|
||||
// Generic functions used in 'request' file @see src/templates/core/request.hbs for more info
|
||||
Handlebars.registerPartial('functions/catchErrors', Handlebars.template(functionCatchErrors));
|
||||
Handlebars.registerPartial('functions/getFormData', Handlebars.template(functionGetFormData));
|
||||
Handlebars.registerPartial('functions/getToken', Handlebars.template(functionGetToken));
|
||||
Handlebars.registerPartial('functions/getQueryString', Handlebars.template(functionGetQueryString));
|
||||
Handlebars.registerPartial('functions/getUrl', Handlebars.template(functionGetUrl));
|
||||
Handlebars.registerPartial('functions/isBinary', Handlebars.template(functionIsBinary));
|
||||
Handlebars.registerPartial('functions/isBlob', Handlebars.template(functionIsBlob));
|
||||
Handlebars.registerPartial('functions/isDefined', Handlebars.template(functionIsDefined));
|
||||
Handlebars.registerPartial('functions/isString', Handlebars.template(functionIsString));
|
||||
Handlebars.registerPartial('functions/isStringWithValue', Handlebars.template(functionIsStringWithValue));
|
||||
Handlebars.registerPartial('functions/isSuccess', Handlebars.template(functionIsSuccess));
|
||||
Handlebars.registerPartial('functions/resolve', Handlebars.template(functionResolve));
|
||||
|
||||
// Specific files for the fetch client implementation
|
||||
Handlebars.registerPartial('fetch/getHeaders', Handlebars.template(fetchGetHeaders));
|
||||
|
||||
@ -46,28 +46,32 @@ export async function writeClient(
|
||||
throw new Error(`Output folder is not a subdirectory of the current working directory`);
|
||||
}
|
||||
|
||||
await rmdir(outputPath);
|
||||
await mkdir(outputPath);
|
||||
|
||||
if (exportCore) {
|
||||
await rmdir(outputPathCore);
|
||||
await mkdir(outputPathCore);
|
||||
await writeClientCore(client, templates, outputPathCore, httpClient);
|
||||
}
|
||||
|
||||
if (exportServices) {
|
||||
await rmdir(outputPathServices);
|
||||
await mkdir(outputPathServices);
|
||||
await writeClientServices(client.services, templates, outputPathServices, httpClient, useUnionTypes, useOptions);
|
||||
}
|
||||
|
||||
if (exportSchemas) {
|
||||
await rmdir(outputPathSchemas);
|
||||
await mkdir(outputPathSchemas);
|
||||
await writeClientSchemas(client.models, templates, outputPathSchemas, httpClient, useUnionTypes);
|
||||
}
|
||||
|
||||
if (exportModels) {
|
||||
await rmdir(outputPathModels);
|
||||
await mkdir(outputPathModels);
|
||||
await writeClientModels(client.models, templates, outputPathModels, httpClient, useUnionTypes);
|
||||
}
|
||||
|
||||
await writeClientIndex(client, templates, outputPath, useUnionTypes, exportCore, exportServices, exportModels, exportSchemas);
|
||||
if (exportCore || exportServices || exportSchemas || exportModels) {
|
||||
await mkdir(outputPath);
|
||||
await writeClientIndex(client, templates, outputPath, useUnionTypes, exportCore, exportServices, exportModels, exportSchemas);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,18 +57,27 @@ exports[`v2 should generate: ./test/generated/v2/core/OpenAPI.ts 1`] = `
|
||||
"/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
type Resolver<T> = () => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
interface Config {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
TOKEN: string | (() => Promise<string>);
|
||||
TOKEN?: string | Resolver<string>;
|
||||
USERNAME?: string | Resolver<string>;
|
||||
PASSWORD?: string | Resolver<string>;
|
||||
HEADERS?: Headers | Resolver<Headers>;
|
||||
}
|
||||
|
||||
export const OpenAPI: Config = {
|
||||
BASE: 'http://localhost:3000/base',
|
||||
VERSION: '1.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
TOKEN: '',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
};"
|
||||
`;
|
||||
|
||||
@ -89,6 +98,10 @@ function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
function isStringWithValue(value: any): value is string {
|
||||
return isString(value) && value !== '';
|
||||
}
|
||||
|
||||
function isBlob(value: any): value is Blob {
|
||||
return value instanceof Blob;
|
||||
}
|
||||
@ -134,24 +147,35 @@ function getFormData(params: Record<string, any>): FormData {
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function getToken(): Promise<string> {
|
||||
if (typeof OpenAPI.TOKEN === 'function') {
|
||||
return OpenAPI.TOKEN();
|
||||
type Resolver<T> = () => Promise<T>;
|
||||
|
||||
async function resolve<T>(resolver?: T | Resolver<T>): Promise<T | undefined> {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)();
|
||||
}
|
||||
return OpenAPI.TOKEN;
|
||||
return resolver;
|
||||
}
|
||||
|
||||
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
...OpenAPI.HEADERS,
|
||||
...options.headers,
|
||||
});
|
||||
|
||||
const token = await getToken();
|
||||
if (isDefined(token) && token !== '') {
|
||||
const token = await resolve(OpenAPI.TOKEN);
|
||||
const username = await resolve(OpenAPI.USERNAME);
|
||||
const password = await resolve(OpenAPI.PASSWORD);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers.append('Authorization', \`Bearer \${token}\`);
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = btoa(\`\${username}:\${password}\`);
|
||||
headers.append('Authorization', \`Basic \${credentials}\`);
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (isBlob(options.body)) {
|
||||
headers.append('Content-Type', options.body.type || 'application/octet-stream');
|
||||
@ -2354,18 +2378,27 @@ exports[`v3 should generate: ./test/generated/v3/core/OpenAPI.ts 1`] = `
|
||||
"/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
type Resolver<T> = () => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
interface Config {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
TOKEN: string | (() => Promise<string>);
|
||||
TOKEN?: string | Resolver<string>;
|
||||
USERNAME?: string | Resolver<string>;
|
||||
PASSWORD?: string | Resolver<string>;
|
||||
HEADERS?: Headers | Resolver<Headers>;
|
||||
}
|
||||
|
||||
export const OpenAPI: Config = {
|
||||
BASE: 'http://localhost:3000/base',
|
||||
VERSION: '1.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
TOKEN: '',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
};"
|
||||
`;
|
||||
|
||||
@ -2386,6 +2419,10 @@ function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
function isStringWithValue(value: any): value is string {
|
||||
return isString(value) && value !== '';
|
||||
}
|
||||
|
||||
function isBlob(value: any): value is Blob {
|
||||
return value instanceof Blob;
|
||||
}
|
||||
@ -2431,24 +2468,35 @@ function getFormData(params: Record<string, any>): FormData {
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function getToken(): Promise<string> {
|
||||
if (typeof OpenAPI.TOKEN === 'function') {
|
||||
return OpenAPI.TOKEN();
|
||||
type Resolver<T> = () => Promise<T>;
|
||||
|
||||
async function resolve<T>(resolver?: T | Resolver<T>): Promise<T | undefined> {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)();
|
||||
}
|
||||
return OpenAPI.TOKEN;
|
||||
return resolver;
|
||||
}
|
||||
|
||||
async function getHeaders(options: ApiRequestOptions): Promise<Headers> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/json',
|
||||
...OpenAPI.HEADERS,
|
||||
...options.headers,
|
||||
});
|
||||
|
||||
const token = await getToken();
|
||||
if (isDefined(token) && token !== '') {
|
||||
const token = await resolve(OpenAPI.TOKEN);
|
||||
const username = await resolve(OpenAPI.USERNAME);
|
||||
const password = await resolve(OpenAPI.PASSWORD);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers.append('Authorization', \`Bearer \${token}\`);
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = btoa(\`\${username}:\${password}\`);
|
||||
headers.append('Authorization', \`Basic \${credentials}\`);
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (isBlob(options.body)) {
|
||||
headers.append('Content-Type', options.body.type || 'application/octet-stream');
|
||||
|
||||
@ -26,11 +26,24 @@ describe('v3.fetch', () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { OpenAPI, SimpleService } = window.api;
|
||||
OpenAPI.TOKEN = window.tokenRequest;
|
||||
OpenAPI.USERNAME = undefined;
|
||||
OpenAPI.PASSWORD = undefined;
|
||||
return await SimpleService.getCallWithoutParametersAndResponse();
|
||||
});
|
||||
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
|
||||
});
|
||||
|
||||
it('uses credentials', async () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { OpenAPI, SimpleService } = window.api;
|
||||
OpenAPI.TOKEN = undefined;
|
||||
OpenAPI.USERNAME = 'username';
|
||||
OpenAPI.PASSWORD = 'password';
|
||||
return await SimpleService.getCallWithoutParametersAndResponse();
|
||||
});
|
||||
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||
});
|
||||
|
||||
it('complexService', async () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { ComplexService } = window.api;
|
||||
|
||||
@ -26,11 +26,24 @@ describe('v3.fetch', () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { OpenAPI, SimpleService } = window.api;
|
||||
OpenAPI.TOKEN = window.tokenRequest;
|
||||
OpenAPI.USERNAME = undefined;
|
||||
OpenAPI.PASSWORD = undefined;
|
||||
return await SimpleService.getCallWithoutParametersAndResponse();
|
||||
});
|
||||
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
|
||||
});
|
||||
|
||||
it('uses credentials', async () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { OpenAPI, SimpleService } = window.api;
|
||||
OpenAPI.TOKEN = undefined;
|
||||
OpenAPI.USERNAME = 'username';
|
||||
OpenAPI.PASSWORD = 'password';
|
||||
return await SimpleService.getCallWithoutParametersAndResponse();
|
||||
});
|
||||
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||
});
|
||||
|
||||
it('complexService', async () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { ComplexService } = window.api;
|
||||
|
||||
@ -20,11 +20,22 @@ describe('v3.node', () => {
|
||||
const { OpenAPI, SimpleService } = require('./generated/v3/node/index.js');
|
||||
const tokenRequest = jest.fn().mockResolvedValue('MY_TOKEN')
|
||||
OpenAPI.TOKEN = tokenRequest;
|
||||
OpenAPI.USERNAME = undefined;
|
||||
OpenAPI.PASSWORD = undefined;
|
||||
const result = await SimpleService.getCallWithoutParametersAndResponse();
|
||||
expect(tokenRequest.mock.calls.length).toBe(1);
|
||||
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
|
||||
});
|
||||
|
||||
it('uses credentials', async () => {
|
||||
const { OpenAPI, SimpleService } = require('./generated/v3/node/index.js');
|
||||
OpenAPI.TOKEN = undefined;
|
||||
OpenAPI.USERNAME = 'username';
|
||||
OpenAPI.PASSWORD = 'password';
|
||||
const result = await SimpleService.getCallWithoutParametersAndResponse();
|
||||
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||
});
|
||||
|
||||
it('complexService', async () => {
|
||||
const { ComplexService } = require('./generated/v3/node/index.js');
|
||||
const result = await ComplexService.complexTypes({
|
||||
|
||||
@ -26,11 +26,24 @@ describe('v3.xhr', () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { OpenAPI, SimpleService } = window.api;
|
||||
OpenAPI.TOKEN = window.tokenRequest;
|
||||
OpenAPI.USERNAME = undefined;
|
||||
OpenAPI.PASSWORD = undefined;
|
||||
return await SimpleService.getCallWithoutParametersAndResponse();
|
||||
});
|
||||
expect(result.headers.authorization).toBe('Bearer MY_TOKEN');
|
||||
});
|
||||
|
||||
it('uses credentials', async () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { OpenAPI, SimpleService } = window.api;
|
||||
OpenAPI.TOKEN = undefined;
|
||||
OpenAPI.USERNAME = 'username';
|
||||
OpenAPI.PASSWORD = 'password';
|
||||
return await SimpleService.getCallWithoutParametersAndResponse();
|
||||
});
|
||||
expect(result.headers.authorization).toBe('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||
});
|
||||
|
||||
it('complexService', async () => {
|
||||
const result = await browser.evaluate(async () => {
|
||||
const { ComplexService } = window.api;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user