mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: Add manual login option and password reset link for SSO (#6328)
* consolidate IUserSettings in workflow and add allowSSOManualLogin * add pw reset link to owners ui
This commit is contained in:
parent
8f0ff460b1
commit
77e3f1551d
|
@ -21,6 +21,7 @@ import type {
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
FeatureFlags,
|
FeatureFlags,
|
||||||
|
IUserSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
|
@ -478,13 +479,6 @@ export interface IPersonalizationSurveyAnswers {
|
||||||
workArea: string[] | string | null;
|
workArea: string[] | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserSettings {
|
|
||||||
isOnboarded?: boolean;
|
|
||||||
showUserActivationSurvey?: boolean;
|
|
||||||
firstSuccessfulWorkflowId?: string;
|
|
||||||
userActivated?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IActiveDirectorySettings {
|
export interface IActiveDirectorySettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,10 @@ export class AuthController {
|
||||||
// attempt to fetch user data with the credentials, but don't log in yet
|
// attempt to fetch user data with the credentials, but don't log in yet
|
||||||
const preliminaryUser = await handleEmailLogin(email, password);
|
const preliminaryUser = await handleEmailLogin(email, password);
|
||||||
// if the user is an owner, continue with the login
|
// if the user is an owner, continue with the login
|
||||||
if (preliminaryUser?.globalRole?.name === 'owner') {
|
if (
|
||||||
|
preliminaryUser?.globalRole?.name === 'owner' ||
|
||||||
|
preliminaryUser?.settings?.allowSSOManualLogin
|
||||||
|
) {
|
||||||
user = preliminaryUser;
|
user = preliminaryUser;
|
||||||
usedAuthenticationMethod = 'email';
|
usedAuthenticationMethod = 'email';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { IsNull, MoreThanOrEqual, Not } from 'typeorm';
|
import { IsNull, MoreThanOrEqual, Not } from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { Get, Post, RestController } from '@/decorators';
|
import { Get, Post, RestController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
|
@ -25,6 +24,7 @@ import type { IDatabaseCollections, IExternalHooksClass, IInternalHooksClass } f
|
||||||
import { issueCookie } from '@/auth/jwt';
|
import { issueCookie } from '@/auth/jwt';
|
||||||
import { isLdapEnabled } from '@/Ldap/helpers';
|
import { isLdapEnabled } from '@/Ldap/helpers';
|
||||||
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
|
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
|
||||||
@RestController()
|
@RestController()
|
||||||
export class PasswordResetController {
|
export class PasswordResetController {
|
||||||
|
@ -103,7 +103,10 @@ export class PasswordResetController {
|
||||||
relations: ['authIdentities', 'globalRole'],
|
relations: ['authIdentities', 'globalRole'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSamlCurrentAuthenticationMethod() && user?.globalRole.name !== 'owner') {
|
if (
|
||||||
|
isSamlCurrentAuthenticationMethod() &&
|
||||||
|
!(user?.globalRole.name === 'owner' || user?.settings?.allowSSOManualLogin === true)
|
||||||
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Request to send password reset email failed because login is handled by SAML',
|
'Request to send password reset email failed because login is handled by SAML',
|
||||||
);
|
);
|
||||||
|
@ -126,18 +129,9 @@ export class PasswordResetController {
|
||||||
throw new UnprocessableRequestError('forgotPassword.ldapUserPasswordResetUnavailable');
|
throw new UnprocessableRequestError('forgotPassword.ldapUserPasswordResetUnavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
user.resetPasswordToken = uuid();
|
|
||||||
|
|
||||||
const { id, firstName, lastName, resetPasswordToken } = user;
|
|
||||||
|
|
||||||
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200;
|
|
||||||
|
|
||||||
await this.userRepository.update(id, { resetPasswordToken, resetPasswordTokenExpiration });
|
|
||||||
|
|
||||||
const baseUrl = getInstanceBaseUrl();
|
const baseUrl = getInstanceBaseUrl();
|
||||||
const url = new URL(`${baseUrl}/change-password`);
|
const { id, firstName, lastName } = user;
|
||||||
url.searchParams.append('userId', id);
|
const url = UserService.generatePasswordResetUrl(user);
|
||||||
url.searchParams.append('token', resetPasswordToken);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.mailer.passwordReset({
|
await this.mailer.passwordReset({
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||||
import { Authorized, NoAuthRequired, Delete, Get, Post, RestController } from '@/decorators';
|
import { Authorized, NoAuthRequired, Delete, Get, Post, RestController, Patch } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
addInviteLinkToUser,
|
addInviteLinkToUser,
|
||||||
generateUserInviteUrl,
|
generateUserInviteUrl,
|
||||||
|
@ -20,7 +20,7 @@ import { issueCookie } from '@/auth/jwt';
|
||||||
import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper';
|
import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import type { Config } from '@/config';
|
import type { Config } from '@/config';
|
||||||
import { UserRequest } from '@/requests';
|
import { UserRequest, UserSettingsUpdatePayload } from '@/requests';
|
||||||
import type { UserManagementMailer } from '@/UserManagement/email';
|
import type { UserManagementMailer } from '@/UserManagement/email';
|
||||||
import type {
|
import type {
|
||||||
PublicUser,
|
PublicUser,
|
||||||
|
@ -40,6 +40,8 @@ import type {
|
||||||
SharedWorkflowRepository,
|
SharedWorkflowRepository,
|
||||||
UserRepository,
|
UserRepository,
|
||||||
} from '@db/repositories';
|
} from '@db/repositories';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
|
||||||
@Authorized(['global', 'owner'])
|
@Authorized(['global', 'owner'])
|
||||||
@RestController('/users')
|
@RestController('/users')
|
||||||
|
@ -355,6 +357,38 @@ export class UsersController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
|
@Get('/:id/password-reset-link')
|
||||||
|
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
|
||||||
|
const user = await this.userRepository.findOneOrFail({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundError('User not found');
|
||||||
|
}
|
||||||
|
const link = await UserService.generatePasswordResetUrl(user);
|
||||||
|
return {
|
||||||
|
link,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorized(['global', 'owner'])
|
||||||
|
@Patch('/:id/settings')
|
||||||
|
async updateUserSettings(req: UserRequest.UserSettingsUpdate) {
|
||||||
|
const payload = plainToInstance(UserSettingsUpdatePayload, req.body);
|
||||||
|
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
await UserService.updateUserSettings(id, payload);
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOneOrFail({
|
||||||
|
select: ['settings'],
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return user.settings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a user. Optionally, designate a transferee for their workflows and credentials.
|
* Delete a user. Optionally, designate a transferee for their workflows and credentials.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -11,14 +11,14 @@ import {
|
||||||
BeforeInsert,
|
BeforeInsert,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { IsEmail, IsString, Length } from 'class-validator';
|
import { IsEmail, IsString, Length } from 'class-validator';
|
||||||
import type { IUser } from 'n8n-workflow';
|
import type { IUser, IUserSettings } from 'n8n-workflow';
|
||||||
import { Role } from './Role';
|
import { Role } from './Role';
|
||||||
import type { SharedWorkflow } from './SharedWorkflow';
|
import type { SharedWorkflow } from './SharedWorkflow';
|
||||||
import type { SharedCredentials } from './SharedCredentials';
|
import type { SharedCredentials } from './SharedCredentials';
|
||||||
import { NoXss } from '../utils/customValidators';
|
import { NoXss } from '../utils/customValidators';
|
||||||
import { objectRetriever, lowerCaser } from '../utils/transformers';
|
import { objectRetriever, lowerCaser } from '../utils/transformers';
|
||||||
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
|
||||||
import type { IPersonalizationSurveyAnswers, IUserSettings } from '@/Interfaces';
|
import type { IPersonalizationSurveyAnswers } from '@/Interfaces';
|
||||||
import type { AuthIdentity } from './AuthIdentity';
|
import type { AuthIdentity } from './AuthIdentity';
|
||||||
|
|
||||||
export const MIN_PASSWORD_LENGTH = 8;
|
export const MIN_PASSWORD_LENGTH = 8;
|
||||||
|
|
|
@ -40,6 +40,10 @@ export class UserSettingsUpdatePayload {
|
||||||
@IsBoolean({ message: 'userActivated should be a boolean' })
|
@IsBoolean({ message: 'userActivated should be a boolean' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
userActivated: boolean;
|
userActivated: boolean;
|
||||||
|
|
||||||
|
@IsBoolean({ message: 'allowSSOManualLogin should be a boolean' })
|
||||||
|
@IsOptional()
|
||||||
|
allowSSOManualLogin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthlessRequest<
|
export type AuthlessRequest<
|
||||||
|
@ -250,6 +254,14 @@ export declare namespace UserRequest {
|
||||||
{ limit?: number; offset?: number; cursor?: string; includeRole?: boolean }
|
{ limit?: number; offset?: number; cursor?: string; includeRole?: boolean }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type PasswordResetLink = AuthenticatedRequest<{ id: string }, {}, {}, {}>;
|
||||||
|
|
||||||
|
export type UserSettingsUpdate = AuthenticatedRequest<
|
||||||
|
{ id: string },
|
||||||
|
{},
|
||||||
|
UserSettingsUpdatePayload
|
||||||
|
>;
|
||||||
|
|
||||||
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
||||||
|
|
||||||
export type Update = AuthlessRequest<
|
export type Update = AuthlessRequest<
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import type { EntityManager, FindOptionsWhere } from 'typeorm';
|
import type { EntityManager, FindOptionsWhere } from 'typeorm';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import type { IUserSettings } from '@/Interfaces';
|
import type { IUserSettings } from 'n8n-workflow';
|
||||||
|
import { getInstanceBaseUrl } from '../UserManagement/UserManagementHelper';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
static async get(where: FindOptionsWhere<User>): Promise<User | null> {
|
static async get(where: FindOptionsWhere<User>): Promise<User | null> {
|
||||||
|
@ -22,4 +24,17 @@ export class UserService {
|
||||||
});
|
});
|
||||||
return Db.collections.User.update(id, { settings: { ...currentSettings, ...userSettings } });
|
return Db.collections.User.update(id, { settings: { ...currentSettings, ...userSettings } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async generatePasswordResetUrl(user: User): Promise<string> {
|
||||||
|
user.resetPasswordToken = uuid();
|
||||||
|
const { id, resetPasswordToken } = user;
|
||||||
|
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200;
|
||||||
|
await Db.collections.User.update(id, { resetPasswordToken, resetPasswordTokenExpiration });
|
||||||
|
|
||||||
|
const baseUrl = getInstanceBaseUrl();
|
||||||
|
const url = new URL(`${baseUrl}/change-password`);
|
||||||
|
url.searchParams.append('userId', id);
|
||||||
|
url.searchParams.append('token', resetPasswordToken);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isOwner">
|
<div v-if="!isOwner">
|
||||||
<n8n-text v-if="signInType" size="small" color="text-light">
|
<n8n-text v-if="signInType" size="small" color="text-light">
|
||||||
Sign-in type: {{ signInType }}
|
Sign-in type:
|
||||||
|
{{
|
||||||
|
isSamlLoginEnabled
|
||||||
|
? settings?.allowSSOManualLogin
|
||||||
|
? $locale.baseText('settings.sso') + ' + ' + signInType
|
||||||
|
: $locale.baseText('settings.sso')
|
||||||
|
: signInType
|
||||||
|
}}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,6 +78,14 @@ export default defineComponent({
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
isSamlLoginEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classes(): Record<string, boolean> {
|
classes(): Record<string, boolean> {
|
||||||
|
|
|
@ -7,7 +7,11 @@
|
||||||
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
|
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
|
||||||
:data-test-id="`user-list-item-${user.email}`"
|
:data-test-id="`user-list-item-${user.email}`"
|
||||||
>
|
>
|
||||||
<n8n-user-info v-bind="user" :isCurrentUser="currentUserId === user.id" />
|
<n8n-user-info
|
||||||
|
v-bind="user"
|
||||||
|
:isCurrentUser="currentUserId === user.id"
|
||||||
|
:isSamlLoginEnabled="isSamlLoginEnabled"
|
||||||
|
/>
|
||||||
<div :class="$style.badgeContainer">
|
<div :class="$style.badgeContainer">
|
||||||
<n8n-badge v-if="user.isOwner" theme="tertiary" bold>
|
<n8n-badge v-if="user.isOwner" theme="tertiary" bold>
|
||||||
{{ t('nds.auth.roles.owner') }}
|
{{ t('nds.auth.roles.owner') }}
|
||||||
|
@ -67,6 +71,10 @@ export default defineComponent({
|
||||||
type: Array as PropType<UserAction[]>,
|
type: Array as PropType<UserAction[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
isSamlLoginEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedUsers(): IUser[] {
|
sortedUsers(): IUser[] {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import type {
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
IUserManagementSettings,
|
IUserManagementSettings,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
|
IUserSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { SignInType } from './constants';
|
import type { SignInType } from './constants';
|
||||||
import type {
|
import type {
|
||||||
|
@ -561,12 +562,7 @@ export interface IUserResponse {
|
||||||
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
signInType?: SignInType;
|
signInType?: SignInType;
|
||||||
settings?: {
|
settings?: IUserSettings;
|
||||||
isOnboarded?: boolean;
|
|
||||||
showUserActivationSurvey?: boolean;
|
|
||||||
firstSuccessfulWorkflowId?: string;
|
|
||||||
userActivated?: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentUserResponse extends IUserResponse {
|
export interface CurrentUserResponse extends IUserResponse {
|
||||||
|
|
|
@ -105,7 +105,15 @@ export async function updateCurrentUserSettings(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
settings: IUserResponse['settings'],
|
settings: IUserResponse['settings'],
|
||||||
): Promise<IUserResponse['settings']> {
|
): Promise<IUserResponse['settings']> {
|
||||||
return makeRestApiRequest(context, 'PATCH', '/me/settings', settings);
|
return makeRestApiRequest(context, 'PATCH', '/me/settings', settings as IDataObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOtherUserSettings(
|
||||||
|
context: IRestApiContext,
|
||||||
|
userId: string,
|
||||||
|
settings: IUserResponse['settings'],
|
||||||
|
): Promise<IUserResponse['settings']> {
|
||||||
|
return makeRestApiRequest(context, 'PATCH', `/users/${userId}/settings`, settings as IDataObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCurrentUserPassword(
|
export async function updateCurrentUserPassword(
|
||||||
|
@ -144,6 +152,13 @@ export async function getInviteLink(
|
||||||
return makeRestApiRequest(context, 'GET', `/users/${id}/invite-link`);
|
return makeRestApiRequest(context, 'GET', `/users/${id}/invite-link`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPasswordResetLink(
|
||||||
|
context: IRestApiContext,
|
||||||
|
{ id }: { id: string },
|
||||||
|
): Promise<{ link: string }> {
|
||||||
|
return makeRestApiRequest(context, 'GET', `/users/${id}/password-reset-link`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function submitPersonalizationSurvey(
|
export async function submitPersonalizationSurvey(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
params: IPersonalizationLatestVersion,
|
params: IPersonalizationLatestVersion,
|
||||||
|
|
|
@ -1169,6 +1169,9 @@
|
||||||
"settings.users.actions.delete": "Delete User",
|
"settings.users.actions.delete": "Delete User",
|
||||||
"settings.users.actions.reinvite": "Resend Invite",
|
"settings.users.actions.reinvite": "Resend Invite",
|
||||||
"settings.users.actions.copyInviteLink": "Copy Invite Link",
|
"settings.users.actions.copyInviteLink": "Copy Invite Link",
|
||||||
|
"settings.users.actions.copyPasswordResetLink": "Copy Password Reset Link",
|
||||||
|
"settings.users.actions.allowSSOManualLogin": "Allow Manual Login",
|
||||||
|
"settings.users.actions.disallowSSOManualLogin": "Disallow Manual Login",
|
||||||
"settings.users.deleteWorkflowsAndCredentials": "Delete their workflows and credentials",
|
"settings.users.deleteWorkflowsAndCredentials": "Delete their workflows and credentials",
|
||||||
"settings.users.emailInvitesSent": "An invite email was sent to {emails}",
|
"settings.users.emailInvitesSent": "An invite email was sent to {emails}",
|
||||||
"settings.users.emailInvitesSentError": "Could not invite {emails}",
|
"settings.users.emailInvitesSentError": "Could not invite {emails}",
|
||||||
|
@ -1187,6 +1190,12 @@
|
||||||
"settings.users.inviteXUser.inviteUrl": "Create {count} invite links",
|
"settings.users.inviteXUser.inviteUrl": "Create {count} invite links",
|
||||||
"settings.users.inviteUrlCreated": "Invite link copied to clipboard",
|
"settings.users.inviteUrlCreated": "Invite link copied to clipboard",
|
||||||
"settings.users.inviteUrlCreated.message": "Send the invite link to your invitee for activation",
|
"settings.users.inviteUrlCreated.message": "Send the invite link to your invitee for activation",
|
||||||
|
"settings.users.passwordResetUrlCreated": "Password reset link copied to clipboard",
|
||||||
|
"settings.users.passwordResetUrlCreated.message": "Send the reset link to your user for them to reset their password",
|
||||||
|
"settings.users.allowSSOManualLogin": "Manual Login Allowed",
|
||||||
|
"settings.users.allowSSOManualLogin.message": "User can now login manually and through SSO",
|
||||||
|
"settings.users.disallowSSOManualLogin": "Manual Login Disallowed",
|
||||||
|
"settings.users.disallowSSOManualLogin.message": "User must now login through SSO only",
|
||||||
"settings.users.multipleInviteUrlsCreated": "Invite links created",
|
"settings.users.multipleInviteUrlsCreated": "Invite links created",
|
||||||
"settings.users.multipleInviteUrlsCreated.message": "Send the invite links to your invitees for activation",
|
"settings.users.multipleInviteUrlsCreated.message": "Send the invite links to your invitees for activation",
|
||||||
"settings.users.newEmailsToInvite": "New User Email Addresses",
|
"settings.users.newEmailsToInvite": "New User Email Addresses",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
changePassword,
|
changePassword,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
getInviteLink,
|
getInviteLink,
|
||||||
|
getPasswordResetLink,
|
||||||
getUsers,
|
getUsers,
|
||||||
inviteUsers,
|
inviteUsers,
|
||||||
login,
|
login,
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
updateCurrentUserPassword,
|
updateCurrentUserPassword,
|
||||||
updateCurrentUserSettings,
|
updateCurrentUserSettings,
|
||||||
|
updateOtherUserSettings,
|
||||||
validatePasswordToken,
|
validatePasswordToken,
|
||||||
validateSignupToken,
|
validateSignupToken,
|
||||||
} from '@/api/users';
|
} from '@/api/users';
|
||||||
|
@ -251,6 +253,19 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
this.addUsers([this.currentUser]);
|
this.addUsers([this.currentUser]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async updateOtherUserSettings(
|
||||||
|
userId: string,
|
||||||
|
settings: IUserResponse['settings'],
|
||||||
|
): Promise<void> {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const updatedSettings = await updateOtherUserSettings(
|
||||||
|
rootStore.getRestApiContext,
|
||||||
|
userId,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
this.users[userId].settings = updatedSettings;
|
||||||
|
this.addUsers([this.users[userId]]);
|
||||||
|
},
|
||||||
async updateCurrentUserPassword({
|
async updateCurrentUserPassword({
|
||||||
password,
|
password,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
|
@ -288,6 +303,10 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
return getInviteLink(rootStore.getRestApiContext, params);
|
return getInviteLink(rootStore.getRestApiContext, params);
|
||||||
},
|
},
|
||||||
|
async getUserPasswordResetLink(params: { id: string }): Promise<{ link: string }> {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
return getPasswordResetLink(rootStore.getRestApiContext, params);
|
||||||
|
},
|
||||||
async submitPersonalizationSurvey(results: IPersonalizationLatestVersion): Promise<void> {
|
async submitPersonalizationSurvey(results: IPersonalizationLatestVersion): Promise<void> {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
await submitPersonalizationSurvey(rootStore.getRestApiContext, results);
|
await submitPersonalizationSurvey(rootStore.getRestApiContext, results);
|
||||||
|
|
|
@ -50,9 +50,13 @@
|
||||||
:actions="usersListActions"
|
:actions="usersListActions"
|
||||||
:users="usersStore.allUsers"
|
:users="usersStore.allUsers"
|
||||||
:currentUserId="usersStore.currentUserId"
|
:currentUserId="usersStore.currentUserId"
|
||||||
|
:isSamlLoginEnabled="ssoStore.isSamlLoginEnabled"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
@reinvite="onReinvite"
|
@reinvite="onReinvite"
|
||||||
@copyInviteLink="onCopyInviteLink"
|
@copyInviteLink="onCopyInviteLink"
|
||||||
|
@copyPasswordResetLink="onCopyPasswordResetLink"
|
||||||
|
@allowSSOManualLogin="onAllowSSOManualLogin"
|
||||||
|
@disallowSSOManualLogin="onDisallowSSOManualLogin"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,6 +110,22 @@ export default defineComponent({
|
||||||
label: this.$locale.baseText('settings.users.actions.delete'),
|
label: this.$locale.baseText('settings.users.actions.delete'),
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: this.$locale.baseText('settings.users.actions.copyPasswordResetLink'),
|
||||||
|
value: 'copyPasswordResetLink',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$locale.baseText('settings.users.actions.allowSSOManualLogin'),
|
||||||
|
value: 'allowSSOManualLogin',
|
||||||
|
guard: (user) =>
|
||||||
|
this.settingsStore.isSamlLoginEnabled && !user.settings?.allowSSOManualLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$locale.baseText('settings.users.actions.disallowSSOManualLogin'),
|
||||||
|
value: 'disallowSSOManualLogin',
|
||||||
|
guard: (user) =>
|
||||||
|
this.settingsStore.isSamlLoginEnabled && user.settings?.allowSSOManualLogin === true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -152,6 +172,44 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async onCopyPasswordResetLink(userId: string) {
|
||||||
|
const user = this.usersStore.getUserById(userId) as IUser | null;
|
||||||
|
if (user) {
|
||||||
|
const url = await this.usersStore.getUserPasswordResetLink(user);
|
||||||
|
this.copyToClipboard(url.link);
|
||||||
|
|
||||||
|
this.showToast({
|
||||||
|
type: 'success',
|
||||||
|
title: this.$locale.baseText('settings.users.passwordResetUrlCreated'),
|
||||||
|
message: this.$locale.baseText('settings.users.passwordResetUrlCreated.message'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onAllowSSOManualLogin(userId: string) {
|
||||||
|
const user = this.usersStore.getUserById(userId) as IUser | null;
|
||||||
|
if (user?.settings) {
|
||||||
|
user.settings.allowSSOManualLogin = true;
|
||||||
|
await this.usersStore.updateOtherUserSettings(userId, user.settings);
|
||||||
|
|
||||||
|
this.showToast({
|
||||||
|
type: 'success',
|
||||||
|
title: this.$locale.baseText('settings.users.allowSSOManualLogin'),
|
||||||
|
message: this.$locale.baseText('settings.users.allowSSOManualLogin.message'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onDisallowSSOManualLogin(userId: string) {
|
||||||
|
const user = this.usersStore.getUserById(userId) as IUser | null;
|
||||||
|
if (user?.settings) {
|
||||||
|
user.settings.allowSSOManualLogin = false;
|
||||||
|
await this.usersStore.updateOtherUserSettings(userId, user.settings);
|
||||||
|
this.showToast({
|
||||||
|
type: 'success',
|
||||||
|
title: this.$locale.baseText('settings.users.disallowSSOManualLogin'),
|
||||||
|
message: this.$locale.baseText('settings.users.disallowSSOManualLogin.message'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
goToUpgrade() {
|
goToUpgrade() {
|
||||||
this.uiStore.goToUpgrade('users', 'upgrade-users');
|
this.uiStore.goToUpgrade('users', 'upgrade-users');
|
||||||
},
|
},
|
||||||
|
|
|
@ -1961,6 +1961,14 @@ export interface IUserManagementSettings {
|
||||||
authenticationMethod: AuthenticationMethod;
|
authenticationMethod: AuthenticationMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserSettings {
|
||||||
|
isOnboarded?: boolean;
|
||||||
|
showUserActivationSurvey?: boolean;
|
||||||
|
firstSuccessfulWorkflowId?: string;
|
||||||
|
userActivated?: boolean;
|
||||||
|
allowSSOManualLogin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPublicApiSettings {
|
export interface IPublicApiSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
latestVersion: number;
|
latestVersion: number;
|
||||||
|
|
Loading…
Reference in a new issue