1
0
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:
olav 2022-06-22 14:55:43 +02:00 committed by GitHub
parent cecca59f65
commit ab75d4085e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1425 additions and 240 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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",
}
`;

View 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",
}
`;

View File

@ -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",
}
`;

View 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>;

View 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();
});

View 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>;

View 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>;

View 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();
});

View 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>;

View 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>;

View 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>;

View File

@ -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();
});

View File

@ -4,9 +4,9 @@ export const resetPasswordSchema = {
$id: '#/components/schemas/resetPasswordSchema',
type: 'object',
additionalProperties: false,
required: ['email'],
required: ['resetPasswordUrl'],
properties: {
email: {
resetPasswordUrl: {
type: 'string',
},
},

View File

@ -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();
});

View 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();
});

View File

@ -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>;

View File

@ -6,6 +6,7 @@ test('tokenUserSchema', () => {
createdBy: '',
token: '',
role: {
id: 1,
description: '',
name: '',
type: '',

View File

@ -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;

View 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>;

View 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>;

View 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();
});

View 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>;

View 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();
});

View 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>;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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}`);
}
}

View File

@ -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,

View File

@ -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 {};
}
}

View File

@ -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>;
}

View File

@ -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>;

View 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);
});

View File

@ -0,0 +1,3 @@
export const isDefined = <T>(value: T | null | undefined): value is T => {
return value !== null && typeof value !== 'undefined';
};

View File

@ -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')

View File

@ -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 {

View File

@ -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);
});

View File

@ -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');
});

View File

@ -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([]);
}

View File

@ -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}