mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
refactor: add schemas to user admin controller (#1692)
* refactor: add schemas to user admin controller * refactor: remove unused SessionService * refactor: fix search query type confusion * refactor: add schemas to user controller (#1693) * refactor: add schemas to user controller * refactor: fix getAllUserSplashes method name * refactor: name and email should not be required on create * refactor: only some user fields may be updated * refactor: should not require any fields on user update (#1730) * refactor: send 400 instead of 500 on missing username and email * refactor: should not require any fields for user update * refactor: note that earlier versions required name or email * refactor: merge roleDescriptionSchema and roleSchema
This commit is contained in:
parent
cecca59f65
commit
ab75d4085e
@ -38,7 +38,7 @@ export default class UserSplashStore implements IUserSplashStore {
|
||||
this.logger = getLogger('user-splash-store.ts');
|
||||
}
|
||||
|
||||
async getAllUserSplashs(userId: number): Promise<IUserSplash[]> {
|
||||
async getAllUserSplashes(userId: number): Promise<IUserSplash[]> {
|
||||
const userSplash = await this.db
|
||||
.table<IUserSplashTable>(TABLE)
|
||||
.select()
|
||||
|
@ -8,7 +8,6 @@ import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
ICreateUser,
|
||||
IUserLookup,
|
||||
IUserSearch,
|
||||
IUserStore,
|
||||
IUserUpdateFields,
|
||||
} from '../types/stores/user-store';
|
||||
@ -116,7 +115,7 @@ class UserStore implements IUserStore {
|
||||
return users.map(rowToUser);
|
||||
}
|
||||
|
||||
async search(query: IUserSearch): Promise<User[]> {
|
||||
async search(query: string): Promise<User[]> {
|
||||
const users = await this.db
|
||||
.select(USER_COLUMNS_PUBLIC)
|
||||
.from(TABLE)
|
||||
|
@ -8,6 +8,7 @@ import { contextFieldsSchema } from './spec/context-fields-schema';
|
||||
import { createApiTokenSchema } from './spec/create-api-token-schema';
|
||||
import { createFeatureSchema } from './spec/create-feature-schema';
|
||||
import { createStrategySchema } from './spec/create-strategy-schema';
|
||||
import { createUserSchema } from './spec/create-user-schema';
|
||||
import { environmentSchema } from './spec/environment-schema';
|
||||
import { environmentsSchema } from './spec/environments-schema';
|
||||
import { featureEnvironmentSchema } from './spec/feature-environment-schema';
|
||||
@ -22,16 +23,21 @@ import { healthCheckSchema } from './spec/health-check-schema';
|
||||
import { healthOverviewSchema } from './spec/health-overview-schema';
|
||||
import { healthReportSchema } from './spec/health-report-schema';
|
||||
import { legalValueSchema } from './spec/legal-value-schema';
|
||||
import { idSchema } from './spec/id-schema';
|
||||
import { mapValues } from '../util/map-values';
|
||||
import { nameSchema } from './spec/name-schema';
|
||||
import { meSchema } from './spec/me-schema';
|
||||
import { omitKeys } from '../util/omit-keys';
|
||||
import { overrideSchema } from './spec/override-schema';
|
||||
import { parametersSchema } from './spec/parameters-schema';
|
||||
import { passwordSchema } from './spec/password-schema';
|
||||
import { patchSchema } from './spec/patch-schema';
|
||||
import { patchesSchema } from './spec/patches-schema';
|
||||
import { permissionSchema } from './spec/permission-schema';
|
||||
import { projectEnvironmentSchema } from './spec/project-environment-schema';
|
||||
import { projectSchema } from './spec/project-schema';
|
||||
import { projectsSchema } from './spec/projects-schema';
|
||||
import { roleSchema } from './spec/role-schema';
|
||||
import { sortOrderSchema } from './spec/sort-order-schema';
|
||||
import { splashSchema } from './spec/splash-schema';
|
||||
import { strategySchema } from './spec/strategy-schema';
|
||||
@ -45,6 +51,10 @@ import { updateStrategySchema } from './spec/update-strategy-schema';
|
||||
import { updateApiTokenSchema } from './spec/update-api-token-schema';
|
||||
import { updateTagTypeSchema } from './spec/update-tag-type-schema';
|
||||
import { upsertContextFieldSchema } from './spec/upsert-context-field-schema';
|
||||
import { updateUserSchema } from './spec/update-user-schema';
|
||||
import { userSchema } from './spec/user-schema';
|
||||
import { usersSchema } from './spec/users-schema';
|
||||
import { usersSearchSchema } from './spec/users-search-schema';
|
||||
import { validateTagTypeSchema } from './spec/validate-tag-type-schema';
|
||||
import { variantSchema } from './spec/variant-schema';
|
||||
import { variantsSchema } from './spec/variants-schema';
|
||||
@ -57,7 +67,6 @@ import { applicationSchema } from './spec/application-schema';
|
||||
import { applicationsSchema } from './spec/applications-schema';
|
||||
import { tagWithVersionSchema } from './spec/tag-with-version-schema';
|
||||
import { tokenUserSchema } from './spec/token-user-schema';
|
||||
import { roleDescriptionSchema } from './spec/role-description-schema';
|
||||
import { changePasswordSchema } from './spec/change-password-schema';
|
||||
import { validatePasswordSchema } from './spec/validate-password-schema';
|
||||
import { resetPasswordSchema } from './spec/reset-password-schema';
|
||||
@ -66,6 +75,7 @@ import { segmentSchema } from './spec/segment-schema';
|
||||
import { stateSchema } from './spec/state-schema';
|
||||
import { featureTagSchema } from './spec/feature-tag-schema';
|
||||
import { exportParametersSchema } from './spec/export-parameters-schema';
|
||||
import { emailSchema } from './spec/email-schema';
|
||||
|
||||
// All schemas in `openapi/spec` should be listed here.
|
||||
export const schemas = {
|
||||
@ -85,6 +95,8 @@ export const schemas = {
|
||||
createApiTokenSchema,
|
||||
createFeatureSchema,
|
||||
createStrategySchema,
|
||||
createUserSchema,
|
||||
emailSchema,
|
||||
environmentSchema,
|
||||
environmentsSchema,
|
||||
exportParametersSchema,
|
||||
@ -103,15 +115,19 @@ export const schemas = {
|
||||
healthReportSchema,
|
||||
legalValueSchema,
|
||||
nameSchema,
|
||||
idSchema,
|
||||
meSchema,
|
||||
overrideSchema,
|
||||
parametersSchema,
|
||||
passwordSchema,
|
||||
patchSchema,
|
||||
patchesSchema,
|
||||
permissionSchema,
|
||||
projectEnvironmentSchema,
|
||||
projectSchema,
|
||||
projectsSchema,
|
||||
resetPasswordSchema,
|
||||
roleDescriptionSchema,
|
||||
roleSchema,
|
||||
segmentSchema,
|
||||
sortOrderSchema,
|
||||
splashSchema,
|
||||
@ -131,6 +147,10 @@ export const schemas = {
|
||||
upsertContextFieldSchema,
|
||||
validatePasswordSchema,
|
||||
validateTagTypeSchema,
|
||||
updateUserSchema,
|
||||
userSchema,
|
||||
usersSchema,
|
||||
usersSearchSchema,
|
||||
variantSchema,
|
||||
variantsSchema,
|
||||
versionSchema,
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`resetPasswordSchema empty 1`] = `
|
||||
exports[`emailSchema 1`] = `
|
||||
Object {
|
||||
"data": Object {},
|
||||
"errors": Array [
|
||||
@ -14,6 +14,6 @@ Object {
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/resetPasswordSchema",
|
||||
"schema": "#/components/schemas/emailSchema",
|
||||
}
|
||||
`;
|
65
src/lib/openapi/spec/__snapshots__/me-schema.test.ts.snap
Normal file
65
src/lib/openapi/spec/__snapshots__/me-schema.test.ts.snap
Normal file
@ -0,0 +1,65 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`meSchema empty 1`] = `
|
||||
Object {
|
||||
"data": Object {},
|
||||
"errors": Array [
|
||||
Object {
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'user'",
|
||||
"params": Object {
|
||||
"missingProperty": "user",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/meSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`meSchema missing permissions 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"user": Object {
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
"errors": Array [
|
||||
Object {
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'permissions'",
|
||||
"params": Object {
|
||||
"missingProperty": "permissions",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/meSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`meSchema missing splash 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"feedback": Array [],
|
||||
"permissions": Array [],
|
||||
"user": Object {
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
"errors": Array [
|
||||
Object {
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'splash'",
|
||||
"params": Object {
|
||||
"missingProperty": "splash",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/meSchema",
|
||||
}
|
||||
`;
|
@ -1,19 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`roleDescriptionSchema empty 1`] = `
|
||||
exports[`roleSchema 1`] = `
|
||||
Object {
|
||||
"data": Object {},
|
||||
"errors": Array [
|
||||
Object {
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'description'",
|
||||
"message": "must have required property 'id'",
|
||||
"params": Object {
|
||||
"missingProperty": "description",
|
||||
"missingProperty": "id",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/roleDescriptionSchema",
|
||||
"schema": "#/components/schemas/roleSchema",
|
||||
}
|
||||
`;
|
31
src/lib/openapi/spec/create-user-schema.ts
Normal file
31
src/lib/openapi/spec/create-user-schema.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const createUserSchema = {
|
||||
$id: '#/components/schemas/createUserSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['rootRole'],
|
||||
properties: {
|
||||
username: {
|
||||
type: 'string',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
},
|
||||
rootRole: {
|
||||
type: 'number',
|
||||
},
|
||||
sendEmail: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type CreateUserSchema = FromSchema<typeof createUserSchema>;
|
16
src/lib/openapi/spec/email-schema.test.ts
Normal file
16
src/lib/openapi/spec/email-schema.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { EmailSchema } from './email-schema';
|
||||
|
||||
test('emailSchema', () => {
|
||||
const data: EmailSchema = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/emailSchema', data),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/emailSchema', {}),
|
||||
).toMatchSnapshot();
|
||||
});
|
16
src/lib/openapi/spec/email-schema.ts
Normal file
16
src/lib/openapi/spec/email-schema.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const emailSchema = {
|
||||
$id: '#/components/schemas/emailSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['email'],
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type EmailSchema = FromSchema<typeof emailSchema>;
|
16
src/lib/openapi/spec/id-schema.ts
Normal file
16
src/lib/openapi/spec/id-schema.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const idSchema = {
|
||||
$id: '#/components/schemas/idSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type IdSchema = FromSchema<typeof idSchema>;
|
37
src/lib/openapi/spec/me-schema.test.ts
Normal file
37
src/lib/openapi/spec/me-schema.test.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { MeSchema } from './me-schema';
|
||||
|
||||
test('meSchema', () => {
|
||||
const data: MeSchema = {
|
||||
user: { id: 1 },
|
||||
permissions: [{ permission: 'a' }],
|
||||
feedback: [{ userId: 1, feedbackId: 'a', neverShow: false }],
|
||||
splash: { a: true },
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/meSchema', data),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('meSchema empty', () => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/meSchema', {}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('meSchema missing permissions', () => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/meSchema', { user: { id: 1 } }),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('meSchema missing splash', () => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/meSchema', {
|
||||
user: { id: 1 },
|
||||
permissions: [],
|
||||
feedback: [],
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
43
src/lib/openapi/spec/me-schema.ts
Normal file
43
src/lib/openapi/spec/me-schema.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { userSchema } from './user-schema';
|
||||
import { permissionSchema } from './permission-schema';
|
||||
import { feedbackSchema } from './feedback-schema';
|
||||
|
||||
export const meSchema = {
|
||||
$id: '#/components/schemas/meSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['user', 'permissions', 'feedback', 'splash'],
|
||||
properties: {
|
||||
user: {
|
||||
$ref: '#/components/schemas/userSchema',
|
||||
},
|
||||
permissions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/permissionSchema',
|
||||
},
|
||||
},
|
||||
feedback: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/feedbackSchema',
|
||||
},
|
||||
},
|
||||
splash: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
userSchema,
|
||||
permissionSchema,
|
||||
feedbackSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type MeSchema = FromSchema<typeof meSchema>;
|
19
src/lib/openapi/spec/password-schema.ts
Normal file
19
src/lib/openapi/spec/password-schema.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const passwordSchema = {
|
||||
$id: '#/components/schemas/passwordSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['password'],
|
||||
properties: {
|
||||
password: {
|
||||
type: 'string',
|
||||
},
|
||||
confirmPassword: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type PasswordSchema = FromSchema<typeof passwordSchema>;
|
22
src/lib/openapi/spec/permission-schema.ts
Normal file
22
src/lib/openapi/spec/permission-schema.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const permissionSchema = {
|
||||
$id: '#/components/schemas/permissionSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['permission'],
|
||||
properties: {
|
||||
permission: {
|
||||
type: 'string',
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
},
|
||||
environment: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type PermissionSchema = FromSchema<typeof permissionSchema>;
|
@ -1,18 +0,0 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { ResetPasswordSchema } from './reset-password-schema';
|
||||
|
||||
test('resetPasswordSchema', () => {
|
||||
const data: ResetPasswordSchema = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/resetPasswordSchema', data),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('resetPasswordSchema empty', () => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/resetPasswordSchema', {}),
|
||||
).toMatchSnapshot();
|
||||
});
|
@ -4,9 +4,9 @@ export const resetPasswordSchema = {
|
||||
$id: '#/components/schemas/resetPasswordSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['email'],
|
||||
required: ['resetPasswordUrl'],
|
||||
properties: {
|
||||
email: {
|
||||
resetPasswordUrl: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { RoleDescriptionSchema } from './role-description-schema';
|
||||
|
||||
test('roleDescriptionSchema', () => {
|
||||
const data: RoleDescriptionSchema = {
|
||||
description: '',
|
||||
name: '',
|
||||
type: '',
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/roleDescriptionSchema', data),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('roleDescriptionSchema empty', () => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/roleDescriptionSchema', {}),
|
||||
).toMatchSnapshot();
|
||||
});
|
19
src/lib/openapi/spec/role-schema.test.ts
Normal file
19
src/lib/openapi/spec/role-schema.test.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { RoleSchema } from './role-schema';
|
||||
|
||||
test('roleSchema', () => {
|
||||
const data: RoleSchema = {
|
||||
id: 1,
|
||||
description: '',
|
||||
name: '',
|
||||
type: '',
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/roleSchema', data),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/roleSchema', {}),
|
||||
).toMatchSnapshot();
|
||||
});
|
@ -1,22 +1,25 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const roleDescriptionSchema = {
|
||||
$id: '#/components/schemas/roleDescriptionSchema',
|
||||
export const roleSchema = {
|
||||
$id: '#/components/schemas/roleSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['description', 'name', 'type'],
|
||||
required: ['id', 'type', 'name'],
|
||||
properties: {
|
||||
description: {
|
||||
id: {
|
||||
type: 'number',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type RoleDescriptionSchema = FromSchema<typeof roleDescriptionSchema>;
|
||||
export type RoleSchema = FromSchema<typeof roleSchema>;
|
@ -6,6 +6,7 @@ test('tokenUserSchema', () => {
|
||||
createdBy: '',
|
||||
token: '',
|
||||
role: {
|
||||
id: 1,
|
||||
description: '',
|
||||
name: '',
|
||||
type: '',
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { roleDescriptionSchema } from './role-description-schema';
|
||||
import { roleSchema } from './role-schema';
|
||||
|
||||
export const tokenUserSchema = {
|
||||
$id: '#/components/schemas/tokenUserSchema',
|
||||
@ -14,12 +14,12 @@ export const tokenUserSchema = {
|
||||
type: 'string',
|
||||
},
|
||||
role: {
|
||||
$ref: '#/components/schemas/roleDescriptionSchema',
|
||||
$ref: '#/components/schemas/roleSchema',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
roleDescriptionSchema,
|
||||
roleSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
21
src/lib/openapi/spec/update-user-schema.ts
Normal file
21
src/lib/openapi/spec/update-user-schema.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const updateUserSchema = {
|
||||
$id: '#/components/schemas/updateUserSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
rootRole: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type UpdateUserSchema = FromSchema<typeof updateUserSchema>;
|
52
src/lib/openapi/spec/user-schema.ts
Normal file
52
src/lib/openapi/spec/user-schema.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const userSchema = {
|
||||
$id: '#/components/schemas/userSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
},
|
||||
isAPI: {
|
||||
type: 'boolean',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
},
|
||||
imageUrl: {
|
||||
type: 'string',
|
||||
},
|
||||
inviteLink: {
|
||||
type: 'string',
|
||||
},
|
||||
loginAttempts: {
|
||||
type: 'number',
|
||||
},
|
||||
emailSent: {
|
||||
type: 'boolean',
|
||||
},
|
||||
rootRole: {
|
||||
type: 'number',
|
||||
},
|
||||
seenAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
nullable: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type UserSchema = FromSchema<typeof userSchema>;
|
13
src/lib/openapi/spec/users-schema.test.ts
Normal file
13
src/lib/openapi/spec/users-schema.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { UsersSchema } from './users-schema';
|
||||
|
||||
test('usersSchema', () => {
|
||||
const data: UsersSchema = {
|
||||
users: [{ id: 1 }],
|
||||
rootRoles: [{ id: 1, type: 'a', name: 'b' }],
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/usersSchema', data),
|
||||
).toBeUndefined();
|
||||
});
|
32
src/lib/openapi/spec/users-schema.ts
Normal file
32
src/lib/openapi/spec/users-schema.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { userSchema } from './user-schema';
|
||||
import { roleSchema } from './role-schema';
|
||||
|
||||
export const usersSchema = {
|
||||
$id: '#/components/schemas/usersSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['users'],
|
||||
properties: {
|
||||
users: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/userSchema',
|
||||
},
|
||||
},
|
||||
rootRoles: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/roleSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
userSchema,
|
||||
roleSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type UsersSchema = FromSchema<typeof usersSchema>;
|
10
src/lib/openapi/spec/users-search-schema.test.ts
Normal file
10
src/lib/openapi/spec/users-search-schema.test.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { UsersSearchSchema } from './users-search-schema';
|
||||
|
||||
test('usersSchema', () => {
|
||||
const data: UsersSearchSchema = [{ id: 1 }];
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/usersSearchSchema', data),
|
||||
).toBeUndefined();
|
||||
});
|
17
src/lib/openapi/spec/users-search-schema.ts
Normal file
17
src/lib/openapi/spec/users-search-schema.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { userSchema } from './user-schema';
|
||||
|
||||
export const usersSearchSchema = {
|
||||
$id: '#/components/schemas/usersSearchSchema',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/userSchema',
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
userSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type UsersSearchSchema = FromSchema<typeof usersSearchSchema>;
|
@ -8,20 +8,29 @@ import { IUnleashConfig } from '../../types/option';
|
||||
import { EmailService } from '../../services/email-service';
|
||||
import ResetTokenService from '../../services/reset-token-service';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import SessionService from '../../services/session-service';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import SettingService from '../../services/setting-service';
|
||||
import { IUser, SimpleAuthSettings } from '../../server-impl';
|
||||
import { simpleAuthKey } from '../../types/settings/simple-auth-settings';
|
||||
import { anonymise } from '../../util/anonymise';
|
||||
|
||||
interface ICreateUserBody {
|
||||
username: string;
|
||||
email: string;
|
||||
name: string;
|
||||
rootRole: number;
|
||||
sendEmail: boolean;
|
||||
}
|
||||
import { OpenApiService } from '../../services/openapi-service';
|
||||
import { emptyResponse } from '../../openapi/spec/empty-response';
|
||||
import { createRequestSchema, createResponseSchema } from '../../openapi';
|
||||
import { userSchema, UserSchema } from '../../openapi/spec/user-schema';
|
||||
import { serializeDates } from '../../types/serialize-dates';
|
||||
import { usersSchema, UsersSchema } from '../../openapi/spec/users-schema';
|
||||
import {
|
||||
usersSearchSchema,
|
||||
UsersSearchSchema,
|
||||
} from '../../openapi/spec/users-search-schema';
|
||||
import { CreateUserSchema } from '../../openapi/spec/create-user-schema';
|
||||
import { UpdateUserSchema } from '../../openapi/spec/update-user-schema';
|
||||
import { PasswordSchema } from '../../openapi/spec/password-schema';
|
||||
import { IdSchema } from '../../openapi/spec/id-schema';
|
||||
import {
|
||||
resetPasswordSchema,
|
||||
ResetPasswordSchema,
|
||||
} from '../../openapi/spec/reset-password-schema';
|
||||
|
||||
export default class UserAdminController extends Controller {
|
||||
private anonymise: boolean = false;
|
||||
@ -36,10 +45,10 @@ export default class UserAdminController extends Controller {
|
||||
|
||||
private resetTokenService: ResetTokenService;
|
||||
|
||||
private sessionService: SessionService;
|
||||
|
||||
private settingService: SettingService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
readonly unleashUrl: string;
|
||||
|
||||
constructor(
|
||||
@ -49,16 +58,16 @@ export default class UserAdminController extends Controller {
|
||||
accessService,
|
||||
emailService,
|
||||
resetTokenService,
|
||||
sessionService,
|
||||
settingService,
|
||||
openApiService,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
| 'userService'
|
||||
| 'accessService'
|
||||
| 'emailService'
|
||||
| 'resetTokenService'
|
||||
| 'sessionService'
|
||||
| 'settingService'
|
||||
| 'openApiService'
|
||||
>,
|
||||
) {
|
||||
super(config);
|
||||
@ -66,32 +75,165 @@ export default class UserAdminController extends Controller {
|
||||
this.accessService = accessService;
|
||||
this.emailService = emailService;
|
||||
this.resetTokenService = resetTokenService;
|
||||
this.sessionService = sessionService;
|
||||
this.settingService = settingService;
|
||||
this.openApiService = openApiService;
|
||||
this.logger = config.getLogger('routes/user-controller.ts');
|
||||
this.unleashUrl = config.server.unleashUrl;
|
||||
this.anonymise = config.experimental?.anonymiseEventLog;
|
||||
|
||||
this.get('/', this.getUsers, ADMIN);
|
||||
this.get('/search', this.search);
|
||||
this.post('/', this.createUser, ADMIN);
|
||||
this.post('/validate-password', this.validatePassword, NONE);
|
||||
this.get('/:id', this.getUser, ADMIN);
|
||||
this.put('/:id', this.updateUser, ADMIN);
|
||||
this.post('/:id/change-password', this.changePassword, ADMIN);
|
||||
this.delete('/:id', this.deleteUser, ADMIN);
|
||||
this.post('/reset-password', this.resetPassword, ADMIN);
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/validate-password',
|
||||
handler: this.validatePassword,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'validatePassword',
|
||||
requestBody: createRequestSchema('passwordSchema'),
|
||||
responses: { 200: emptyResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/:id/change-password',
|
||||
handler: this.changePassword,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'changePassword',
|
||||
requestBody: createRequestSchema('passwordSchema'),
|
||||
responses: { 200: emptyResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/reset-password',
|
||||
handler: this.resetPassword,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'resetPassword',
|
||||
requestBody: createRequestSchema('idSchema'),
|
||||
responses: {
|
||||
200: createResponseSchema('resetPasswordSchema'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getUsers,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'getUsers',
|
||||
responses: { 200: createResponseSchema('usersSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/search',
|
||||
handler: this.searchUsers,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'searchUsers',
|
||||
responses: { 200: createResponseSchema('usersSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '',
|
||||
handler: this.createUser,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'createUser',
|
||||
requestBody: createRequestSchema('createUserSchema'),
|
||||
responses: { 200: createResponseSchema('userSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:id',
|
||||
handler: this.getUser,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'getUser',
|
||||
responses: { 200: createResponseSchema('userSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'put',
|
||||
path: '/:id',
|
||||
handler: this.updateUser,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'updateUser',
|
||||
requestBody: createRequestSchema('updateUserSchema'),
|
||||
responses: { 200: createResponseSchema('userSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
acceptAnyContentType: true,
|
||||
handler: this.deleteUser,
|
||||
permission: ADMIN,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'deleteUser',
|
||||
responses: { 200: emptyResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async resetPassword(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async resetPassword(
|
||||
req: IAuthRequest<unknown, ResetPasswordSchema, IdSchema>,
|
||||
res: Response<ResetPasswordSchema>,
|
||||
): Promise<void> {
|
||||
const { user } = req;
|
||||
const receiver = req.body.id;
|
||||
const resetPasswordUrl =
|
||||
await this.userService.createResetPasswordEmail(receiver, user);
|
||||
res.json({ resetPasswordUrl });
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
resetPasswordSchema.$id,
|
||||
{ resetPasswordUrl: resetPasswordUrl.toString() },
|
||||
);
|
||||
}
|
||||
|
||||
async getUsers(req: Request, res: Response): Promise<void> {
|
||||
async getUsers(req: Request, res: Response<UsersSchema>): Promise<void> {
|
||||
const users = await this.userService.getAll();
|
||||
const rootRoles = await this.accessService.getRootRoles();
|
||||
const inviteLinks = await this.resetTokenService.getActiveInvitations();
|
||||
@ -101,7 +243,10 @@ export default class UserAdminController extends Controller {
|
||||
return { ...user, inviteLink };
|
||||
});
|
||||
|
||||
res.json({ users: usersWithInviteLinks, rootRoles });
|
||||
this.openApiService.respondWithValidation(200, res, usersSchema.$id, {
|
||||
users: serializeDates(usersWithInviteLinks),
|
||||
rootRoles,
|
||||
});
|
||||
}
|
||||
|
||||
anonymiseUsers(users: IUser[]): IUser[] {
|
||||
@ -113,118 +258,130 @@ export default class UserAdminController extends Controller {
|
||||
}));
|
||||
}
|
||||
|
||||
async search(req: Request, res: Response): Promise<void> {
|
||||
const { q } = req.query as any;
|
||||
try {
|
||||
let users =
|
||||
q && q.length > 1 ? await this.userService.search(q) : [];
|
||||
|
||||
if (this.anonymise) {
|
||||
users = this.anonymiseUsers(users);
|
||||
}
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
res.status(500).send({ msg: 'server errors' });
|
||||
async searchUsers(
|
||||
req: Request,
|
||||
res: Response<UsersSearchSchema>,
|
||||
): Promise<void> {
|
||||
const { q } = req.query;
|
||||
let users =
|
||||
typeof q === 'string' && q.length > 1
|
||||
? await this.userService.search(q)
|
||||
: [];
|
||||
if (this.anonymise) {
|
||||
users = this.anonymiseUsers(users);
|
||||
}
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
usersSearchSchema.$id,
|
||||
serializeDates(users),
|
||||
);
|
||||
}
|
||||
|
||||
async getUser(req: Request, res: Response): Promise<void> {
|
||||
async getUser(req: Request, res: Response<UserSchema>): Promise<void> {
|
||||
const { id } = req.params;
|
||||
const user = await this.userService.getUser(Number(id));
|
||||
res.json(user);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
userSchema.$id,
|
||||
serializeDates(user),
|
||||
);
|
||||
}
|
||||
|
||||
async createUser(
|
||||
req: IAuthRequest<any, any, ICreateUserBody, any>,
|
||||
res: Response,
|
||||
req: IAuthRequest<unknown, unknown, CreateUserSchema>,
|
||||
res: Response<UserSchema>,
|
||||
): Promise<void> {
|
||||
const { username, email, name, rootRole, sendEmail } = req.body;
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const createdUser = await this.userService.createUser(
|
||||
{
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
rootRole,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
const passwordAuthSettings =
|
||||
await this.settingService.get<SimpleAuthSettings>(
|
||||
simpleAuthKey,
|
||||
);
|
||||
|
||||
let inviteLink: string;
|
||||
if (!passwordAuthSettings?.disabled) {
|
||||
const inviteUrl = await this.resetTokenService.createNewUserUrl(
|
||||
createdUser.id,
|
||||
user.email,
|
||||
);
|
||||
inviteLink = inviteUrl.toString();
|
||||
}
|
||||
|
||||
let emailSent = false;
|
||||
const emailConfigured = this.emailService.configured();
|
||||
const reallySendEmail =
|
||||
emailConfigured && (sendEmail !== undefined ? sendEmail : true);
|
||||
if (reallySendEmail) {
|
||||
try {
|
||||
await this.emailService.sendGettingStartedMail(
|
||||
createdUser.name,
|
||||
createdUser.email,
|
||||
this.unleashUrl,
|
||||
inviteLink,
|
||||
);
|
||||
emailSent = true;
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
'email was configured, but sending failed due to: ',
|
||||
e,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'email was not sent to the user because email configuration is lacking',
|
||||
);
|
||||
}
|
||||
|
||||
res.status(201).send({
|
||||
...createdUser,
|
||||
inviteLink: inviteLink || this.unleashUrl,
|
||||
emailSent,
|
||||
const createdUser = await this.userService.createUser(
|
||||
{
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
rootRole,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.warn(e.message);
|
||||
res.status(400).send([{ msg: e.message }]);
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
const passwordAuthSettings =
|
||||
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
||||
|
||||
let inviteLink: string;
|
||||
if (!passwordAuthSettings?.disabled) {
|
||||
const inviteUrl = await this.resetTokenService.createNewUserUrl(
|
||||
createdUser.id,
|
||||
user.email,
|
||||
);
|
||||
inviteLink = inviteUrl.toString();
|
||||
}
|
||||
|
||||
let emailSent = false;
|
||||
const emailConfigured = this.emailService.configured();
|
||||
const reallySendEmail =
|
||||
emailConfigured && (sendEmail !== undefined ? sendEmail : true);
|
||||
|
||||
if (reallySendEmail) {
|
||||
try {
|
||||
await this.emailService.sendGettingStartedMail(
|
||||
createdUser.name,
|
||||
createdUser.email,
|
||||
this.unleashUrl,
|
||||
inviteLink,
|
||||
);
|
||||
emailSent = true;
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
'email was configured, but sending failed due to: ',
|
||||
e,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'email was not sent to the user because email configuration is lacking',
|
||||
);
|
||||
}
|
||||
|
||||
const responseData: UserSchema = {
|
||||
...serializeDates(createdUser),
|
||||
inviteLink: inviteLink || this.unleashUrl,
|
||||
emailSent,
|
||||
rootRole,
|
||||
};
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
201,
|
||||
res,
|
||||
userSchema.$id,
|
||||
responseData,
|
||||
);
|
||||
}
|
||||
|
||||
async updateUser(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async updateUser(
|
||||
req: IAuthRequest<{ id: string }, UserSchema, UpdateUserSchema>,
|
||||
res: Response<UserSchema>,
|
||||
): Promise<void> {
|
||||
const { user, params, body } = req;
|
||||
|
||||
const { id } = params;
|
||||
const { name, email, rootRole } = body;
|
||||
|
||||
try {
|
||||
const updateUser = await this.userService.updateUser(
|
||||
{
|
||||
id: Number(id),
|
||||
name,
|
||||
email,
|
||||
rootRole,
|
||||
},
|
||||
user,
|
||||
);
|
||||
res.status(200).send({ ...updateUser, rootRole });
|
||||
} catch (e) {
|
||||
this.logger.warn(e.message);
|
||||
res.status(400).send([{ msg: e.message }]);
|
||||
}
|
||||
const updateUser = await this.userService.updateUser(
|
||||
{
|
||||
id: Number(id),
|
||||
name,
|
||||
email,
|
||||
rootRole,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(200, res, userSchema.$id, {
|
||||
...serializeDates(updateUser),
|
||||
rootRole,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUser(req: IAuthRequest, res: Response): Promise<void> {
|
||||
@ -235,14 +392,20 @@ export default class UserAdminController extends Controller {
|
||||
res.status(200).send();
|
||||
}
|
||||
|
||||
async validatePassword(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async validatePassword(
|
||||
req: IAuthRequest<unknown, unknown, PasswordSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { password } = req.body;
|
||||
|
||||
this.userService.validatePassword(password);
|
||||
res.status(200).send();
|
||||
}
|
||||
|
||||
async changePassword(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async changePassword(
|
||||
req: IAuthRequest<{ id: string }, unknown, PasswordSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { id } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
|
@ -8,11 +8,13 @@ import UserService from '../../services/user-service';
|
||||
import UserFeedbackService from '../../services/user-feedback-service';
|
||||
import UserSplashService from '../../services/user-splash-service';
|
||||
import { ADMIN, NONE } from '../../types/permissions';
|
||||
|
||||
interface IChangeUserRequest {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
import { OpenApiService } from '../../services/openapi-service';
|
||||
import { createRequestSchema, createResponseSchema } from '../../openapi';
|
||||
import { emptyResponse } from '../../openapi/spec/empty-response';
|
||||
import { meSchema, MeSchema } from '../../openapi/spec/me-schema';
|
||||
import { serializeDates } from '../../types/serialize-dates';
|
||||
import { IUserPermission } from '../../types/stores/access-store';
|
||||
import { PasswordSchema } from '../../openapi/spec/password-schema';
|
||||
|
||||
class UserController extends Controller {
|
||||
private accessService: AccessService;
|
||||
@ -23,6 +25,8 @@ class UserController extends Controller {
|
||||
|
||||
private userSplashService: UserSplashService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
@ -30,12 +34,14 @@ class UserController extends Controller {
|
||||
userService,
|
||||
userFeedbackService,
|
||||
userSplashService,
|
||||
openApiService,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
| 'accessService'
|
||||
| 'userService'
|
||||
| 'userFeedbackService'
|
||||
| 'userSplashService'
|
||||
| 'openApiService'
|
||||
>,
|
||||
) {
|
||||
super(config);
|
||||
@ -43,15 +49,45 @@ class UserController extends Controller {
|
||||
this.userService = userService;
|
||||
this.userFeedbackService = userFeedbackService;
|
||||
this.userSplashService = userSplashService;
|
||||
this.openApiService = openApiService;
|
||||
|
||||
this.get('/', this.getUser);
|
||||
this.post('/change-password', this.updateUserPass, NONE);
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getMe,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'getMe',
|
||||
responses: { 200: createResponseSchema('meSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/change-password',
|
||||
handler: this.changeMyPassword,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'changeMyPassword',
|
||||
requestBody: createRequestSchema('passwordSchema'),
|
||||
responses: {
|
||||
200: emptyResponse,
|
||||
400: { description: 'passwordMismatch' },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getUser(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async getMe(req: IAuthRequest, res: Response<MeSchema>): Promise<void> {
|
||||
res.setHeader('cache-control', 'no-store');
|
||||
const { user } = req;
|
||||
let permissions;
|
||||
let permissions: IUserPermission[];
|
||||
if (this.config.authentication.type === IAuthType.NONE) {
|
||||
permissions = [{ permission: ADMIN }];
|
||||
} else {
|
||||
@ -60,16 +96,25 @@ class UserController extends Controller {
|
||||
const feedback = await this.userFeedbackService.getAllUserFeedback(
|
||||
user,
|
||||
);
|
||||
const splash = await this.userSplashService.getAllUserSplashs(user);
|
||||
const splash = await this.userSplashService.getAllUserSplashes(user);
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ user, permissions, feedback, splash })
|
||||
.end();
|
||||
const responseData: MeSchema = {
|
||||
user: serializeDates(user),
|
||||
permissions,
|
||||
feedback: serializeDates(feedback),
|
||||
splash,
|
||||
};
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
meSchema.$id,
|
||||
responseData,
|
||||
);
|
||||
}
|
||||
|
||||
async updateUserPass(
|
||||
req: IAuthRequest<any, any, IChangeUserRequest, any>,
|
||||
async changeMyPassword(
|
||||
req: IAuthRequest<unknown, unknown, PasswordSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { user } = req;
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
tokenUserSchema,
|
||||
TokenUserSchema,
|
||||
} from '../../openapi/spec/token-user-schema';
|
||||
import { EmailSchema } from '../../openapi/spec/email-schema';
|
||||
|
||||
interface IValidateQuery {
|
||||
token: string;
|
||||
@ -97,14 +98,17 @@ class ResetPasswordController extends Controller {
|
||||
openApiService.validPath({
|
||||
tags: ['other'],
|
||||
operationId: 'sendResetPasswordEmail',
|
||||
requestBody: createRequestSchema('resetPasswordSchema'),
|
||||
requestBody: createRequestSchema('emailSchema'),
|
||||
responses: { 200: emptyResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async sendResetPasswordEmail(req: Request, res: Response): Promise<void> {
|
||||
async sendResetPasswordEmail(
|
||||
req: Request<unknown, unknown, EmailSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { email } = req.body;
|
||||
|
||||
await this.userService.createResetPasswordEmail(email);
|
||||
|
@ -27,6 +27,7 @@ import { roleSchema } from '../schema/role-schema';
|
||||
import { CUSTOM_ROLE_TYPE, ALL_PROJECTS, ALL_ENVS } from '../util/constants';
|
||||
import { DEFAULT_PROJECT } from '../types/project';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import BadDataError from '../error/bad-data-error';
|
||||
|
||||
const { ADMIN } = permissions;
|
||||
|
||||
@ -200,7 +201,7 @@ export class AccessService {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Could not find rootRole=${role}`);
|
||||
throw new BadDataError(`Could not find rootRole=${role}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import assert from 'assert';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import owasp from 'owasp-password-strength-test';
|
||||
import Joi from 'joi';
|
||||
@ -19,13 +18,15 @@ import { IUnleashStores } from '../types/stores';
|
||||
import PasswordUndefinedError from '../error/password-undefined';
|
||||
import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IUserSearch, IUserStore } from '../types/stores/user-store';
|
||||
import { IUserStore } from '../types/stores/user-store';
|
||||
import { RoleName } from '../types/model';
|
||||
import SettingService from './setting-service';
|
||||
import { SimpleAuthSettings } from '../server-impl';
|
||||
import { simpleAuthKey } from '../types/settings/simple-auth-settings';
|
||||
import DisabledError from '../error/disabled-error';
|
||||
import PasswordMismatch from '../error/password-mismatch';
|
||||
import BadDataError from '../error/bad-data-error';
|
||||
import { isDefined } from '../util/isDefined';
|
||||
|
||||
const systemUser = new User({ id: -1, username: 'system' });
|
||||
|
||||
@ -54,11 +55,14 @@ export interface ILoginUserRequest {
|
||||
interface IUserWithRole extends IUser {
|
||||
rootRole: number;
|
||||
}
|
||||
|
||||
interface IRoleDescription {
|
||||
id: number;
|
||||
description: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ITokenUser extends IUpdateUser {
|
||||
createdBy: string;
|
||||
token: string;
|
||||
@ -171,7 +175,7 @@ class UserService {
|
||||
return { ...user, rootRole: roleId };
|
||||
}
|
||||
|
||||
async search(query: IUserSearch): Promise<IUser[]> {
|
||||
async search(query: string): Promise<IUser[]> {
|
||||
return this.store.search(query);
|
||||
}
|
||||
|
||||
@ -183,7 +187,9 @@ class UserService {
|
||||
{ username, email, name, password, rootRole }: ICreateUser,
|
||||
updatedBy?: User,
|
||||
): Promise<IUser> {
|
||||
assert.ok(username || email, 'You must specify username or email');
|
||||
if (!username && !email) {
|
||||
throw new BadDataError('You must specify username or email');
|
||||
}
|
||||
|
||||
if (email) {
|
||||
Joi.assert(email, Joi.string().email(), 'Email');
|
||||
@ -236,15 +242,25 @@ class UserService {
|
||||
{ id, name, email, rootRole }: IUpdateUser,
|
||||
updatedBy?: User,
|
||||
): Promise<IUser> {
|
||||
Joi.assert(email, Joi.string().email(), 'Email');
|
||||
|
||||
const preUser = await this.store.get(id);
|
||||
|
||||
if (email) {
|
||||
Joi.assert(email, Joi.string().email(), 'Email');
|
||||
}
|
||||
|
||||
if (rootRole) {
|
||||
await this.accessService.setUserRootRole(id, rootRole);
|
||||
}
|
||||
|
||||
const user = await this.store.update(id, { name, email });
|
||||
const payload: Partial<IUser> = {
|
||||
name: name || preUser.name,
|
||||
email: email || preUser.email,
|
||||
};
|
||||
|
||||
// Empty updates will throw, so make sure we have something to update.
|
||||
const user = Object.values(payload).some(isDefined)
|
||||
? await this.store.update(id, payload)
|
||||
: preUser;
|
||||
|
||||
await this.eventStore.store({
|
||||
type: USER_UPDATED,
|
||||
@ -359,6 +375,7 @@ class UserService {
|
||||
name: user.name,
|
||||
id: user.id,
|
||||
role: {
|
||||
id: user.rootRole,
|
||||
description: role.role.description,
|
||||
type: role.role.type,
|
||||
name: role.role.name,
|
||||
|
@ -20,13 +20,13 @@ export default class UserSplashService {
|
||||
this.logger = getLogger('services/user-splash-service.js');
|
||||
}
|
||||
|
||||
async getAllUserSplashs(user: User): Promise<Object> {
|
||||
async getAllUserSplashes(user: User): Promise<Record<string, boolean>> {
|
||||
if (user.isAPI) {
|
||||
return [];
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const splashs = (
|
||||
await this.userSplashStore.getAllUserSplashs(user.id)
|
||||
return (
|
||||
await this.userSplashStore.getAllUserSplashes(user.id)
|
||||
).reduce(
|
||||
(splashObject, splash) => ({
|
||||
...splashObject,
|
||||
@ -34,10 +34,8 @@ export default class UserSplashService {
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return splashs;
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export interface IUserSplashKey {
|
||||
}
|
||||
|
||||
export interface IUserSplashStore extends Store<IUserSplash, IUserSplashKey> {
|
||||
getAllUserSplashs(userId: number): Promise<IUserSplash[]>;
|
||||
getAllUserSplashes(userId: number): Promise<IUserSplash[]>;
|
||||
getSplash(userId: number, splashId: string): Promise<IUserSplash>;
|
||||
updateSplash(splash: IUserSplash): Promise<IUserSplash>;
|
||||
}
|
||||
|
@ -14,12 +14,6 @@ export interface IUserLookup {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface IUserSearch {
|
||||
name?: string;
|
||||
username?: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface IUserUpdateFields {
|
||||
name?: string;
|
||||
email?: string;
|
||||
@ -30,7 +24,7 @@ export interface IUserStore extends Store<IUser, number> {
|
||||
insert(user: ICreateUser): Promise<IUser>;
|
||||
upsert(user: ICreateUser): Promise<IUser>;
|
||||
hasUser(idQuery: IUserLookup): Promise<number | undefined>;
|
||||
search(query: IUserSearch): Promise<IUser[]>;
|
||||
search(query: string): Promise<IUser[]>;
|
||||
getAllWithId(userIdList: number[]): Promise<IUser[]>;
|
||||
getByQuery(idQuery: IUserLookup): Promise<IUser>;
|
||||
getPasswordHash(userId: number): Promise<string>;
|
||||
|
8
src/lib/util/isDefined.test.ts
Normal file
8
src/lib/util/isDefined.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { isDefined } from './isDefined';
|
||||
|
||||
test('isDefined', () => {
|
||||
expect(isDefined(null)).toEqual(false);
|
||||
expect(isDefined(undefined)).toEqual(false);
|
||||
expect(isDefined(0)).toEqual(true);
|
||||
expect(isDefined(false)).toEqual(true);
|
||||
});
|
3
src/lib/util/isDefined.ts
Normal file
3
src/lib/util/isDefined.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const isDefined = <T>(value: T | null | undefined): value is T => {
|
||||
return value !== null && typeof value !== 'undefined';
|
||||
};
|
@ -11,6 +11,8 @@ import { IEventStore } from '../../../../lib/types/stores/event-store';
|
||||
import { IUserStore } from '../../../../lib/types/stores/user-store';
|
||||
import { RoleName } from '../../../../lib/types/model';
|
||||
import { IRoleStore } from 'lib/types/stores/role-store';
|
||||
import { randomId } from '../../../../lib/util/random-id';
|
||||
import { omitKeys } from '../../../../lib/util/omit-keys';
|
||||
|
||||
let stores;
|
||||
let db;
|
||||
@ -133,6 +135,19 @@ test('requires known root role', async () => {
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('should require username or email on create', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({ rootRole: adminRole.id })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.details[0].message).toEqual(
|
||||
'You must specify username or email',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('update user name', async () => {
|
||||
const { body } = await app.request
|
||||
.post('/api/admin/user-admin')
|
||||
@ -157,6 +172,24 @@ test('update user name', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should not require any fields on update', async () => {
|
||||
const { body: created } = await app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({ email: `${randomId()}@example.com`, rootRole: editorRole.id })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
|
||||
const { body: updated } = await app.request
|
||||
.put(`/api/admin/user-admin/${created.id}`)
|
||||
.send({})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
expect(updated).toEqual(
|
||||
omitKeys(created, 'emailSent', 'inviteLink', 'rootRole'),
|
||||
);
|
||||
});
|
||||
|
||||
test('get a single user', async () => {
|
||||
const { body } = await app.request
|
||||
.post('/api/admin/user-admin')
|
||||
|
@ -488,6 +488,45 @@ Object {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"createUserSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"email": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"name": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"password": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"rootRole": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"sendEmail": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"username": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"rootRole",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"emailSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"email": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"email",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"environmentSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -953,6 +992,18 @@ Object {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"idSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"id": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"id",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"legalValueSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -968,6 +1019,39 @@ Object {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"meSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"feedback": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/feedbackSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"permissions": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/permissionSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"splash": Object {
|
||||
"additionalProperties": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"user": Object {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"user",
|
||||
"permissions",
|
||||
"feedback",
|
||||
"splash",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"nameSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -1005,6 +1089,21 @@ Object {
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"passwordSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"confirmPassword": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"password": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"password",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"patchSchema": Object {
|
||||
"properties": Object {
|
||||
"from": Object {
|
||||
@ -1037,6 +1136,24 @@ Object {
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"permissionSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"environment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"permission": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"project": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"permission",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"projectEnvironmentSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -1108,21 +1225,24 @@ Object {
|
||||
"resetPasswordSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"email": Object {
|
||||
"resetPasswordUrl": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"email",
|
||||
"resetPasswordUrl",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"roleDescriptionSchema": Object {
|
||||
"roleSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"description": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"id": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"name": Object {
|
||||
"type": "string",
|
||||
},
|
||||
@ -1131,9 +1251,9 @@ Object {
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"description",
|
||||
"name",
|
||||
"id",
|
||||
"type",
|
||||
"name",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
@ -1385,7 +1505,7 @@ Object {
|
||||
"type": "string",
|
||||
},
|
||||
"role": Object {
|
||||
"$ref": "#/components/schemas/roleDescriptionSchema",
|
||||
"$ref": "#/components/schemas/roleSchema",
|
||||
},
|
||||
"token": Object {
|
||||
"type": "string",
|
||||
@ -1550,6 +1670,21 @@ Object {
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"updateUserSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"email": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"name": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"rootRole": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"upsertContextFieldSchema": Object {
|
||||
"properties": Object {
|
||||
"description": Object {
|
||||
@ -1576,6 +1711,81 @@ Object {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"userSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"createdAt": Object {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
},
|
||||
"email": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"emailSent": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"id": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"imageUrl": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"inviteLink": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"isAPI": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"loginAttempts": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"name": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"rootRole": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"seenAt": Object {
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"username": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"id",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"usersSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"rootRoles": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/roleSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"users": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"users",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"usersSearchSchema": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"validatePasswordSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -4098,6 +4308,301 @@ Object {
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user": Object {
|
||||
"get": Object {
|
||||
"operationId": "getMe",
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/meSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "meSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user-admin": Object {
|
||||
"get": Object {
|
||||
"operationId": "getUsers",
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/usersSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "usersSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
"post": Object {
|
||||
"operationId": "createUser",
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/createUserSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "createUserSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "userSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user-admin/reset-password": Object {
|
||||
"post": Object {
|
||||
"operationId": "resetPassword",
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/idSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "idSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/resetPasswordSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "resetPasswordSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user-admin/search": Object {
|
||||
"get": Object {
|
||||
"operationId": "searchUsers",
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/usersSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "usersSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user-admin/validate-password": Object {
|
||||
"post": Object {
|
||||
"operationId": "validatePassword",
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/passwordSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "passwordSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"description": "emptyResponse",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user-admin/{id}": Object {
|
||||
"delete": Object {
|
||||
"operationId": "deleteUser",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"description": "emptyResponse",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
"get": Object {
|
||||
"operationId": "getUser",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "userSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
"put": Object {
|
||||
"operationId": "updateUser",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/updateUserSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "updateUserSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/userSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "userSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user-admin/{id}/change-password": Object {
|
||||
"post": Object {
|
||||
"operationId": "changePassword",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/passwordSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "passwordSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"description": "emptyResponse",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/user/change-password": Object {
|
||||
"post": Object {
|
||||
"operationId": "changeMyPassword",
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/passwordSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "passwordSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"description": "emptyResponse",
|
||||
},
|
||||
"400": Object {
|
||||
"description": "passwordMismatch",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/auth/reset/password": Object {
|
||||
"post": Object {
|
||||
"operationId": "changePassword",
|
||||
@ -4129,11 +4634,11 @@ Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/resetPasswordSchema",
|
||||
"$ref": "#/components/schemas/emailSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "resetPasswordSchema",
|
||||
"description": "emailSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
|
@ -188,16 +188,13 @@ test('updating a user without an email should not strip the email', async () =>
|
||||
rootRole: adminRole.id,
|
||||
});
|
||||
|
||||
try {
|
||||
await userService.updateUser({
|
||||
id: user.id,
|
||||
email: null,
|
||||
name: 'some',
|
||||
});
|
||||
} catch (e) {}
|
||||
await userService.updateUser({
|
||||
id: user.id,
|
||||
email: null,
|
||||
name: 'some',
|
||||
});
|
||||
|
||||
const updatedUser = await userService.getUser(user.id);
|
||||
|
||||
expect(updatedUser.email).toBe(email);
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,9 @@ test('should create userSplash', async () => {
|
||||
userId: currentUser.id,
|
||||
seen: false,
|
||||
});
|
||||
const userSplashs = await userSplashStore.getAllUserSplashs(currentUser.id);
|
||||
const userSplashs = await userSplashStore.getAllUserSplashes(
|
||||
currentUser.id,
|
||||
);
|
||||
expect(userSplashs).toHaveLength(1);
|
||||
expect(userSplashs[0].splashId).toBe('some-id');
|
||||
});
|
||||
|
2
src/test/fixtures/fake-user-splash-store.ts
vendored
2
src/test/fixtures/fake-user-splash-store.ts
vendored
@ -6,7 +6,7 @@ import {
|
||||
|
||||
export default class FakeUserSplashStore implements IUserSplashStore {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getAllUserSplashs(userId: number): Promise<IUserSplash[]> {
|
||||
getAllUserSplashes(userId: number): Promise<IUserSplash[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
@ -202,7 +202,8 @@ Updates use with new fields
|
||||
**Notes**
|
||||
|
||||
- `userId` is required as a url path parameter.
|
||||
- You must provide _at least_ either `name` or `email`. All other fields are entirely optional. Only provided fields are updated.
|
||||
- All fields are optional. Only provided fields are updated.
|
||||
- Note that earlier versions of Unleash required either `name` or `email` to be set.
|
||||
|
||||
### Delete a user {#delete-a-user}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user