mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
(feat) improved security when changing password
This commit is contained in:
parent
1a6605fbef
commit
137b9f3c1b
@ -12,7 +12,7 @@ CommitMsg:
|
|||||||
|
|
||||||
MessageFormat:
|
MessageFormat:
|
||||||
enabled: true
|
enabled: true
|
||||||
pattern: ^(\((doc|bug|feat|security|dev|i18n|api|test|quality|ui|merge)\) [\w ]++(\n\n.+)?)|(Version (\d+\.?)+)|(Merge branch .*)
|
pattern: ^(\((doc|bug|feat|security|dev|i18n|api|test|quality|ui|merge|wip)\) [\w ]++(\n\n.+)?)|(Version (\d+\.?)+)|(Merge branch .*)
|
||||||
expected_pattern_message: (doc|bug|feat|security|dev|i18n|api|test|quality|ui|merge) title\n\ndescription
|
expected_pattern_message: (doc|bug|feat|security|dev|i18n|api|test|quality|ui|merge|wip) title\n\ndescription
|
||||||
sample_message: (bug) no validation on date\n\nThe birthdate was not validated...
|
sample_message: (bug) no validation on date\n\nThe birthdate was not validated...
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
- Ability to define multiple accounting journal codes
|
- Ability to define multiple accounting journal codes
|
||||||
- OpenAPI endpoint to fetch accounting data
|
- OpenAPI endpoint to fetch accounting data
|
||||||
- Add reservation deadline parameter (#414)
|
- Add reservation deadline parameter (#414)
|
||||||
|
- Verify current password at server side when changing password
|
||||||
|
- Password strengh indicator
|
||||||
- Updated OpenAPI documentation
|
- Updated OpenAPI documentation
|
||||||
- Updated OpenID Connect documentation
|
- Updated OpenID Connect documentation
|
||||||
- OpenAPI users endpoint offer ability to filter by created_after
|
- OpenAPI users endpoint offer ability to filter by created_after
|
||||||
|
@ -47,7 +47,7 @@ class API::MembersController < API::ApiController
|
|||||||
authorize @member
|
authorize @member
|
||||||
members_service = Members::MembersService.new(@member)
|
members_service = Members::MembersService.new(@member)
|
||||||
|
|
||||||
if members_service.update(user_params)
|
if members_service.update(user_params, current_user, params[:user][:current_password])
|
||||||
# Update password without logging out
|
# Update password without logging out
|
||||||
bypass_sign_in(@member) unless current_user.id != params[:id].to_i
|
bypass_sign_in(@member) unless current_user.id != params[:id].to_i
|
||||||
render :show, status: :ok, location: member_path(@member)
|
render :show, status: :ok, location: member_path(@member)
|
||||||
@ -235,7 +235,7 @@ class API::MembersController < API::ApiController
|
|||||||
],
|
],
|
||||||
statistic_profile_attributes: %i[id gender birthday])
|
statistic_profile_attributes: %i[id gender birthday])
|
||||||
|
|
||||||
elsif current_user.admin? || current_user.manager?
|
elsif current_user.privileged?
|
||||||
params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id,
|
params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id,
|
||||||
tag_ids: [],
|
tag_ids: [],
|
||||||
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Devise controller used for the "forgotten password" feature
|
# Devise controller used for the "forgotten password" feature and to check the current's user password
|
||||||
class PasswordsController < Devise::PasswordsController
|
class PasswordsController < Devise::PasswordsController
|
||||||
# POST /users/password.json
|
# POST /users/password.json
|
||||||
def create
|
def create
|
||||||
self.resource = resource_class.send_reset_password_instructions(resource_params)
|
self.resource = resource_class.send_reset_password_instructions(resource_params)
|
||||||
yield resource if block_given?
|
yield resource if block_given?
|
||||||
|
|
||||||
if successfully_sent?(resource)
|
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) if successfully_sent?(resource)
|
||||||
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /password/verify
|
# POST /password/verify
|
||||||
def verify
|
def verify
|
||||||
current_user.valid_password?(params[:password]) ? head(200) : head(404)
|
current_user.valid_password?(params[:password]) ? head(:ok) : head(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import Authentication from '../../api/authentication';
|
import Authentication from '../../api/authentication';
|
||||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||||
import { PasswordInput } from './password-input';
|
import { PasswordInput } from './password-input';
|
||||||
import { FormState } from 'react-hook-form/dist/types/form';
|
import { FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||||
import MemberAPI from '../../api/member';
|
import MemberAPI from '../../api/member';
|
||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
|
|
||||||
@ -18,13 +18,14 @@ interface ChangePasswordProp<TFieldValues> {
|
|||||||
currentFormPassword: string,
|
currentFormPassword: string,
|
||||||
formState: FormState<TFieldValues>,
|
formState: FormState<TFieldValues>,
|
||||||
user: User,
|
user: User,
|
||||||
|
setValue: UseFormSetValue<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component shows a button that trigger a modal dialog to verify the user's current password.
|
* This component shows a button that trigger a modal dialog to verify the user's current password.
|
||||||
* If the user's current password is correct, the modal dialog is closed and the button is replaced by a form to set the new password.
|
* If the user's current password is correct, the modal dialog is closed and the button is replaced by a form to set the new password.
|
||||||
*/
|
*/
|
||||||
export const ChangePassword = <TFieldValues extends FieldValues>({ register, onError, currentFormPassword, formState, user }: ChangePasswordProp<TFieldValues>) => {
|
export const ChangePassword = <TFieldValues extends FieldValues>({ register, onError, currentFormPassword, formState, user, setValue }: ChangePasswordProp<TFieldValues>) => {
|
||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
|
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
|
||||||
@ -68,6 +69,7 @@ export const ChangePassword = <TFieldValues extends FieldValues>({ register, onE
|
|||||||
return handleSubmit((data: { password: string }) => {
|
return handleSubmit((data: { password: string }) => {
|
||||||
Authentication.verifyPassword(data.password).then(res => {
|
Authentication.verifyPassword(data.password).then(res => {
|
||||||
if (res) {
|
if (res) {
|
||||||
|
setValue('current_password', data.password);
|
||||||
setIsConfirmedPassword(true);
|
setIsConfirmedPassword(true);
|
||||||
toggleConfirmationModal();
|
toggleConfirmationModal();
|
||||||
} else {
|
} else {
|
||||||
@ -85,6 +87,7 @@ export const ChangePassword = <TFieldValues extends FieldValues>({ register, onE
|
|||||||
{t('app.shared.change_password.change_my_password')}
|
{t('app.shared.change_password.change_my_password')}
|
||||||
</FabButton>}
|
</FabButton>}
|
||||||
{isConfirmedPassword && <div className="password-fields">
|
{isConfirmedPassword && <div className="password-fields">
|
||||||
|
<FormInput register={register} id="current_password" type="hidden" label="current password" />
|
||||||
<PasswordInput register={register} currentFormPassword={currentFormPassword} formState={formState} />
|
<PasswordInput register={register} currentFormPassword={currentFormPassword} formState={formState} />
|
||||||
</div>}
|
</div>}
|
||||||
<FabModal isOpen={isModalOpen} toggleModal={toggleConfirmationModal} title={t('app.shared.change_password.change_my_password')} closeButton>
|
<FabModal isOpen={isModalOpen} toggleModal={toggleConfirmationModal} title={t('app.shared.change_password.change_my_password')} closeButton>
|
||||||
|
@ -3,6 +3,10 @@ import { FieldValues } from 'react-hook-form/dist/types/fields';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FormInput } from '../form/form-input';
|
import { FormInput } from '../form/form-input';
|
||||||
import { FormState } from 'react-hook-form/dist/types/form';
|
import { FormState } from 'react-hook-form/dist/types/form';
|
||||||
|
import { PasswordStrength } from './password-strength';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Eye, EyeSlash } from 'phosphor-react';
|
||||||
|
|
||||||
interface PasswordInputProps<TFieldValues> {
|
interface PasswordInputProps<TFieldValues> {
|
||||||
register: UseFormRegister<TFieldValues>,
|
register: UseFormRegister<TFieldValues>,
|
||||||
@ -16,9 +20,32 @@ interface PasswordInputProps<TFieldValues> {
|
|||||||
export const PasswordInput = <TFieldValues extends FieldValues>({ register, currentFormPassword, formState }: PasswordInputProps<TFieldValues>) => {
|
export const PasswordInput = <TFieldValues extends FieldValues>({ register, currentFormPassword, formState }: PasswordInputProps<TFieldValues>) => {
|
||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
|
const [password, setPassword] = useState<string>(null);
|
||||||
|
const [inputType, setInputType] = useState<'password'|'text'>('password');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user types a password
|
||||||
|
*/
|
||||||
|
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch the password characters between hidden and displayed
|
||||||
|
*/
|
||||||
|
const toggleShowPassword = () => {
|
||||||
|
if (inputType === 'text') {
|
||||||
|
setInputType('password');
|
||||||
|
} else {
|
||||||
|
setInputType('text');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="password-input">
|
<div className="password-input">
|
||||||
<FormInput id="password" register={register}
|
<FormInput id="password" register={register}
|
||||||
|
addOn={inputType === 'password' ? <Eye size={24} /> : <EyeSlash size={24} />}
|
||||||
|
addOnAction={toggleShowPassword}
|
||||||
rules={{
|
rules={{
|
||||||
required: true,
|
required: true,
|
||||||
validate: (value: string) => {
|
validate: (value: string) => {
|
||||||
@ -29,9 +56,11 @@ export const PasswordInput = <TFieldValues extends FieldValues>({ register, curr
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
label={t('app.shared.password_input.new_password')}
|
label={t('app.shared.password_input.new_password')}
|
||||||
tooltip={t('app.shared.password_input.help')}
|
tooltip={t('app.shared.password_input.help')}
|
||||||
type="password" />
|
type={inputType} />
|
||||||
|
<PasswordStrength password={password} />
|
||||||
<FormInput id="password_confirmation"
|
<FormInput id="password_confirmation"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{
|
rules={{
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
|
||||||
|
import zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
||||||
|
import { debounce as _debounce } from 'lodash';
|
||||||
|
import LocaliseLib from '../../lib/localise';
|
||||||
|
import type { ZxcvbnResult } from '@zxcvbn-ts/core/src/types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface PasswordStrengthProps {
|
||||||
|
password?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPECIAL_CHARS = ['!', '#', '$', '%', '&', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~', "'", '`', '"'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a visual indicator of the password strength
|
||||||
|
*/
|
||||||
|
export const PasswordStrength: React.FC<PasswordStrengthProps> = ({ password }) => {
|
||||||
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
|
const [strength, setStrength] = useState<ZxcvbnResult>(null);
|
||||||
|
const [hasRequirements, setHasRequirements] = useState<boolean>(false);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* zxcvbn library options
|
||||||
|
* @see https://zxcvbn-ts.github.io/zxcvbn/guide/getting-started/
|
||||||
|
*/
|
||||||
|
const options = {
|
||||||
|
translations: null,
|
||||||
|
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
||||||
|
dictionary: LocaliseLib.zxcvbnDictionnaries()
|
||||||
|
};
|
||||||
|
zxcvbnOptions.setOptions(options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the strength of the given password and update the result in memory
|
||||||
|
*/
|
||||||
|
const updateStrength = () => {
|
||||||
|
if (typeof password === 'string') {
|
||||||
|
if (checkRequirements()) {
|
||||||
|
setHasRequirements(true);
|
||||||
|
const result = zxcvbn(password);
|
||||||
|
setStrength(result);
|
||||||
|
} else {
|
||||||
|
setHasRequirements(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the provided password meet the minimal requirements
|
||||||
|
*/
|
||||||
|
const checkRequirements = (): boolean => {
|
||||||
|
if (typeof password === 'string') {
|
||||||
|
const chars = password.split('');
|
||||||
|
return (chars.some(c => SPECIAL_CHARS.includes(c)) &&
|
||||||
|
!!password.match(/[A-Z]/) &&
|
||||||
|
!!password.match(/[a-z]/) &&
|
||||||
|
!!password.match(/[0-9]/) &&
|
||||||
|
password.length >= 12);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(_debounce(updateStrength, 500), [password]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="password-strength">
|
||||||
|
{password && !hasRequirements && <>
|
||||||
|
<span className="requirements-error">{t('app.shared.password_strength.not_in_requirements')}</span>
|
||||||
|
</>}
|
||||||
|
{hasRequirements && strength && <>
|
||||||
|
<div className={`strength-bar strength-${strength.score}`} />
|
||||||
|
<span>{t(`app.shared.password_strength.${strength.score}`)}</span>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -255,7 +255,8 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
|||||||
onError={onError}
|
onError={onError}
|
||||||
currentFormPassword={output.password}
|
currentFormPassword={output.password}
|
||||||
user={user}
|
user={user}
|
||||||
formState={formState} />}
|
formState={formState}
|
||||||
|
setValue={setValue} />}
|
||||||
{action === 'create' && <PasswordInput register={register}
|
{action === 'create' && <PasswordInput register={register}
|
||||||
currentFormPassword={output.password}
|
currentFormPassword={output.password}
|
||||||
formState={formState} />}
|
formState={formState} />}
|
||||||
|
46
app/frontend/src/javascript/lib/localise.ts
Normal file
46
app/frontend/src/javascript/lib/localise.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { IFablab } from '../models/fablab';
|
||||||
|
import zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
||||||
|
import zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
||||||
|
import zxcvbnDePackage from '@zxcvbn-ts/language-de';
|
||||||
|
import zxcvbnEsPackage from '@zxcvbn-ts/language-es-es';
|
||||||
|
import zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
|
||||||
|
import zxcvbnPtPackage from '@zxcvbn-ts/language-pt-br';
|
||||||
|
|
||||||
|
declare let Fablab: IFablab;
|
||||||
|
/**
|
||||||
|
* Localization specific handlers
|
||||||
|
*/
|
||||||
|
export default class LocaliseLib {
|
||||||
|
/**
|
||||||
|
* Bind the dictionnaries for the zxcvbn lib, to the current locale configuration of the app (APP_LOCALE).
|
||||||
|
*/
|
||||||
|
static zxcvbnDictionnaries = () => {
|
||||||
|
switch (Fablab.locale) {
|
||||||
|
case 'de':
|
||||||
|
return {
|
||||||
|
...zxcvbnCommonPackage.dictionary,
|
||||||
|
...zxcvbnDePackage.dictionary
|
||||||
|
};
|
||||||
|
case 'es':
|
||||||
|
return {
|
||||||
|
...zxcvbnCommonPackage.dictionary,
|
||||||
|
...zxcvbnEsPackage.dictionary
|
||||||
|
};
|
||||||
|
case 'fr':
|
||||||
|
return {
|
||||||
|
...zxcvbnCommonPackage.dictionary,
|
||||||
|
...zxcvbnFrPackage.dictionary
|
||||||
|
};
|
||||||
|
case 'pt':
|
||||||
|
return {
|
||||||
|
...zxcvbnCommonPackage.dictionary,
|
||||||
|
...zxcvbnPtPackage.dictionary
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
...zxcvbnCommonPackage.dictionary,
|
||||||
|
...zxcvbnEnPackage.dictionary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -19,6 +19,7 @@ export interface User {
|
|||||||
need_completion?: boolean,
|
need_completion?: boolean,
|
||||||
ip_address?: string,
|
ip_address?: string,
|
||||||
mapped_from_sso?: string[],
|
mapped_from_sso?: string[],
|
||||||
|
current_password?: string,
|
||||||
password?: string,
|
password?: string,
|
||||||
password_confirmation?: string,
|
password_confirmation?: string,
|
||||||
cgu?: boolean, // Accepted terms and conditions?
|
cgu?: boolean, // Accepted terms and conditions?
|
||||||
|
@ -132,8 +132,10 @@
|
|||||||
@import "modules/trainings/training-form";
|
@import "modules/trainings/training-form";
|
||||||
@import "modules/user/avatar";
|
@import "modules/user/avatar";
|
||||||
@import "modules/user/avatar-input";
|
@import "modules/user/avatar-input";
|
||||||
|
@import "modules/user/change-password";
|
||||||
@import "modules/user/gender-input";
|
@import "modules/user/gender-input";
|
||||||
@import "modules/user/member-select";
|
@import "modules/user/member-select";
|
||||||
|
@import "modules/user/password-strength";
|
||||||
@import "modules/user/user-profile-form";
|
@import "modules/user/user-profile-form";
|
||||||
@import "modules/user/user-validation";
|
@import "modules/user/user-validation";
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
.change-password {
|
||||||
|
.password-input {
|
||||||
|
animation: show 600ms 100ms cubic-bezier(0.38, 0.97, 0.56, 0.76) forwards;
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotateX(-90deg);
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes show {
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
.password-strength {
|
||||||
|
margin-top: -1rem;
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.requirements-error {
|
||||||
|
color: var(--alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 1rem;
|
||||||
|
border: 1px solid var(--gray-soft);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
&.strength-0 {
|
||||||
|
background: linear-gradient(to right, var(--alert), white 20%);
|
||||||
|
}
|
||||||
|
&.strength-1 {
|
||||||
|
background: linear-gradient(to right, var(--alert), orange 20%, white 40%);
|
||||||
|
}
|
||||||
|
&.strength-2 {
|
||||||
|
background: linear-gradient(to right, var(--alert), orange 20%, yellow 40%, white 60%);
|
||||||
|
}
|
||||||
|
&.strength-3 {
|
||||||
|
background: linear-gradient(to right, var(--alert), orange 20%, yellow 40%, #5e790f 60%, white 80%);
|
||||||
|
}
|
||||||
|
&.strength-4 {
|
||||||
|
background: linear-gradient(to right, var(--alert), orange 20%, yellow 40%, #5e790f 60%, var(--success) 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-left: 2rem;
|
||||||
|
min-width: fit-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
@ -255,7 +255,7 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def password_complexity
|
def password_complexity
|
||||||
return if password.blank? || SecurePassword.is_secured?(password)
|
return if password.blank? || SecurePassword.secured?(password)
|
||||||
|
|
||||||
errors.add I18n.t('app.public.common.password_is_too_weak'), I18n.t('app.public.common.password_is_too_weak_explanations')
|
errors.add I18n.t('app.public.common.password_is_too_weak'), I18n.t('app.public.common.password_is_too_weak_explanations')
|
||||||
end
|
end
|
||||||
|
@ -16,7 +16,7 @@ class Members::ImportService
|
|||||||
params = row_to_params(row, user, password)
|
params = row_to_params(row, user, password)
|
||||||
if user
|
if user
|
||||||
service = Members::MembersService.new(user)
|
service = Members::MembersService.new(user)
|
||||||
res = service.update(params)
|
res = service.update(params, import.user)
|
||||||
log << { user: user.id, status: 'update', result: res }
|
log << { user: user.id, status: 'update', result: res }
|
||||||
else
|
else
|
||||||
user = User.new(params)
|
user = User.new(params)
|
||||||
@ -26,14 +26,10 @@ class Members::ImportService
|
|||||||
end
|
end
|
||||||
log << user.errors.to_hash unless user.errors.to_hash.empty?
|
log << user.errors.to_hash unless user.errors.to_hash.empty?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
log << e.to_s
|
handle_error(log, e)
|
||||||
Rails.logger.error e
|
|
||||||
Rails.logger.debug e.backtrace
|
|
||||||
end
|
end
|
||||||
rescue ArgumentError => e
|
rescue ArgumentError => e
|
||||||
log << e.to_s
|
handle_error(log, e)
|
||||||
Rails.logger.error e
|
|
||||||
Rails.logger.debug e.backtrace
|
|
||||||
end
|
end
|
||||||
log
|
log
|
||||||
end
|
end
|
||||||
@ -184,4 +180,10 @@ class Members::ImportService
|
|||||||
password
|
password
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_error(log, error)
|
||||||
|
log << error.to_s
|
||||||
|
Rails.logger.error error
|
||||||
|
Rails.logger.debug error.backtrace
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,7 @@ class Members::MembersService
|
|||||||
@member = member
|
@member = member
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(params)
|
def update(params, operator, current_password = nil)
|
||||||
if subscriber_group_change?(params)
|
if subscriber_group_change?(params)
|
||||||
# here a group change is requested but unprocessable, handle the exception
|
# here a group change is requested but unprocessable, handle the exception
|
||||||
@member.errors.add(:group_id, I18n.t('members.unable_to_change_the_group_while_a_subscription_is_running'))
|
@member.errors.add(:group_id, I18n.t('members.unable_to_change_the_group_while_a_subscription_is_running'))
|
||||||
@ -30,6 +30,8 @@ class Members::MembersService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
handle_password(params, operator, current_password)
|
||||||
|
|
||||||
Members::MembersService.handle_organization(params)
|
Members::MembersService.handle_organization(params)
|
||||||
|
|
||||||
not_complete = member.need_completion?
|
not_complete = member.need_completion?
|
||||||
@ -171,4 +173,14 @@ class Members::MembersService
|
|||||||
def user_group_change?(params)
|
def user_group_change?(params)
|
||||||
@member.group_id && params[:group_id] && @member.group_id != params[:group_id].to_i
|
@member.group_id && params[:group_id] && @member.group_id != params[:group_id].to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_password(params, operator, current_password = nil)
|
||||||
|
return unless params[:password] && params[:password_confirmation]
|
||||||
|
|
||||||
|
return if operator.privileged?
|
||||||
|
|
||||||
|
raise SecurityError, 'current password not provided' if current_password.blank?
|
||||||
|
|
||||||
|
raise SecurityError, 'current password does not match' unless @member.valid_password?(current_password)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Ensure the passwords are secure enough
|
||||||
class SecurePassword
|
class SecurePassword
|
||||||
LOWER_LETTERS = ('a'..'z').to_a
|
LOWER_LETTERS = ('a'..'z').to_a
|
||||||
UPPER_LETTERS = ('A'..'Z').to_a
|
UPPER_LETTERS = ('A'..'Z').to_a
|
||||||
DIGITS = ('0'..'9').to_a
|
DIGITS = ('0'..'9').to_a
|
||||||
SPECIAL_CHARS = ["!", "#", "$", "%", "&", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "]", "^", "_", "{", "|", "}", "~", "'", "`", '"']
|
SPECIAL_CHARS = ['!', '#', '$', '%', '&', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', ']', '^', '_', '{',
|
||||||
|
'|', '}', '~', "'", '`', '"'].freeze
|
||||||
|
|
||||||
def self.generate
|
def self.generate
|
||||||
(LOWER_LETTERS.shuffle.first(4) + UPPER_LETTERS.shuffle.first(4) + DIGITS.shuffle.first(4) + SPECIAL_CHARS.shuffle.first(4)).shuffle.join
|
(LOWER_LETTERS.sample(4) + UPPER_LETTERS.sample(4) + DIGITS.sample(4) + SPECIAL_CHARS.sample(4)).shuffle.join
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.is_secured?(password)
|
def self.secured?(password)
|
||||||
password_as_array = password.split("")
|
password_as_array = password.chars
|
||||||
password_as_array.any? {|c| c.in? LOWER_LETTERS } &&
|
password_as_array.any? { |c| c.in? LOWER_LETTERS } &&
|
||||||
password_as_array.any? {|c| c.in? UPPER_LETTERS } &&
|
password_as_array.any? { |c| c.in? UPPER_LETTERS } &&
|
||||||
password_as_array.any? {|c| c.in? DIGITS } &&
|
password_as_array.any? { |c| c.in? DIGITS } &&
|
||||||
password_as_array.any? {|c| c.in? SPECIAL_CHARS }
|
password_as_array.any? { |c| c.in? SPECIAL_CHARS }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -116,6 +116,13 @@ en:
|
|||||||
help: "Your password must be minimum 12 characters long, have at least one uppercase letter, one lowercase letter, one number and one special character."
|
help: "Your password must be minimum 12 characters long, have at least one uppercase letter, one lowercase letter, one number and one special character."
|
||||||
password_too_short: "Password is too short (must be at least 12 characters)"
|
password_too_short: "Password is too short (must be at least 12 characters)"
|
||||||
confirmation_mismatch: "Confirmation mismatch with password."
|
confirmation_mismatch: "Confirmation mismatch with password."
|
||||||
|
password_strength:
|
||||||
|
not_in_requirements: "Your password doesn't meet the minimal requirements"
|
||||||
|
0: "Very weak password"
|
||||||
|
1: "Weak password"
|
||||||
|
2: "Almost ok"
|
||||||
|
3: "Good password"
|
||||||
|
4: "Excellent password"
|
||||||
#project edition form
|
#project edition form
|
||||||
project:
|
project:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
|
@ -81,6 +81,13 @@
|
|||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
"@types/sortablejs": "1",
|
"@types/sortablejs": "1",
|
||||||
"@uirouter/angularjs": "1.0.30",
|
"@uirouter/angularjs": "1.0.30",
|
||||||
|
"@zxcvbn-ts/core": "^2.1.0",
|
||||||
|
"@zxcvbn-ts/language-common": "^2.0.1",
|
||||||
|
"@zxcvbn-ts/language-de": "^2.1.0",
|
||||||
|
"@zxcvbn-ts/language-en": "^2.1.0",
|
||||||
|
"@zxcvbn-ts/language-es-es": "^2.1.1",
|
||||||
|
"@zxcvbn-ts/language-fr": "^2.2.0",
|
||||||
|
"@zxcvbn-ts/language-pt-br": "^2.1.0",
|
||||||
"AngularDevise": "https://github.com/cloudspace/angular_devise.git#1.0.2",
|
"AngularDevise": "https://github.com/cloudspace/angular_devise.git#1.0.2",
|
||||||
"angular": "1.8",
|
"angular": "1.8",
|
||||||
"angular-animate": "1.7",
|
"angular-animate": "1.7",
|
||||||
|
35
yarn.lock
35
yarn.lock
@ -3812,6 +3812,41 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/core@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/core/-/core-2.1.0.tgz#026ffaba5b09cb05ee80f28b0183f75217d267d1"
|
||||||
|
integrity sha512-doxol9xrO7LgyVJhguXe7vO0xthnIYmsOKoDwrLg0Ho2kkpQaVtM+AOQw+BkEiKIqNg1V48eUf4/cTzMElXdiA==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/language-common@^2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-common/-/language-common-2.0.1.tgz#01c371a64e86de417c317b4443aaa0a0f07f917b"
|
||||||
|
integrity sha512-P+v5MA/UNc9nb3FEOEoDgTyIGQc2vLc6m04pdf5YyuNOzrL0iNANhECk2TUp62JbrjouJVodqhMH0j1a8/24Bg==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/language-de@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-de/-/language-de-2.1.0.tgz#c312638d6f520df40ac953cd0c1c3bcc69701bb3"
|
||||||
|
integrity sha512-VAk6D8+1eaeyatFU6Uz9Odiqu58e4VtyWzqdy2EmajAuGzZ+jpZLWtAlRG/qfElAFKR1B7SUp7tHRApzEJywvQ==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/language-en@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-en/-/language-en-2.1.0.tgz#52c797914380546b191e5b915e0e9843116eae18"
|
||||||
|
integrity sha512-I3n4AAbArjPAZtwCrk9MQnSrcj5+9rq8sic2rUU44fP5QaR17Vk8zDt61+R9dnP9ZRsj09aAUYML4Ash05qZjQ==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/language-es-es@^2.1.1":
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-es-es/-/language-es-es-2.1.1.tgz#960b28bf58e537547293d555a36b1c42ef1ce66b"
|
||||||
|
integrity sha512-uDXU/z1df6YGmacFVcFhsvQ2Uu/EbMFCjLeNoM/95vH3GCTb/10eI5IlzjgSP4EG305vd9oNpBy6MODu+9SvNg==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/language-fr@^2.2.0":
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-fr/-/language-fr-2.2.0.tgz#0b7dd93ebba0044fbe733836bc7091b76d42afe1"
|
||||||
|
integrity sha512-KK+vIXm17mZyo7jLmV4T0fT6hh5NOBABdmkCBVpLyXq+rlZpdaz6HgoYLjqq2JbEU3KSZ+gv6qW+2N4dMk3Tlw==
|
||||||
|
|
||||||
|
"@zxcvbn-ts/language-pt-br@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-pt-br/-/language-pt-br-2.1.0.tgz#27676c0ee8df538ffc98bc7bca2817d492c70428"
|
||||||
|
integrity sha512-g4HxJLBf546BSfM4zDq29CNpAmFl2UsgHrEjy6gUA4KBEVqEaYNnMNfvayEtM7PpnzfZjSyLLVVG6S02lR8w+g==
|
||||||
|
|
||||||
"@zxing/text-encoding@0.9.0":
|
"@zxing/text-encoding@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
|
resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
|
||||||
|
Loading…
Reference in New Issue
Block a user