1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

Merge branch 'abuses' into dev

This commit is contained in:
Sylvain 2019-05-21 12:25:43 +02:00
commit 2e4718a8c1
27 changed files with 362 additions and 100 deletions

View File

@ -2,6 +2,7 @@
- Configurable privacy policy and data protection officer
- Alert users on privacy policy update
- Abuses reports management panel
- Fix a bug: (spanish) some translations are not loaded correctly
- Fix a bug: some users may not appear in the admin's general listing
- Fix a bug: updating a setting does not chain new values

View File

@ -0,0 +1,52 @@
/**
* Controller used in abuses management page
*/
Application.Controllers.controller('AbusesController', ['$scope', '$state', 'Abuse', 'abusesPromise', 'dialogs', 'growl', '_t',
function ($scope, $state, Abuse, abusesPromise, dialogs, growl, _t) {
/* PUBLIC SCOPE */
// List of all reported abuses
$scope.abuses = [];
/**
* Callback handling a click on the button: confirm before delete
*/
$scope.confirmProcess = function (abuseId) {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('manage_abuses.confirmation_required'),
msg: _t('manage_abuses.report_will_be_destroyed')
};
}
}
},
function () { // cancel confirmed
Abuse.remove({ id: abuseId }, function () { // successfully canceled
growl.success(_t('manage_abuses.report_removed'));
Abuse.query({}, function (abuses) {
$scope.abuses = abuses.abuses.filter(a => a.signaled_type === 'Project');
});
}
, function () { // error while canceling
growl.error(_t('manage_abuses.failed_to_remove'));
});
}
);
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// we display only abuses related to projects
$scope.abuses = abusesPromise.abuses.filter(a => a.signaled_type === 'Project');
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,2 +0,0 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.

View File

@ -323,7 +323,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
$scope.projectsPagination = new paginationService.Instance(OpenlabProject, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreOpenlabCallback);
return OpenlabProject.query({ q: $scope.search.q, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) {
if (projectsPromise.errors != null) {
growl.error(_t('openlab_search_not_available_at_the_moment'));
growl.error(_t('projects_list.openlab_search_not_available_at_the_moment'));
$scope.openlab.searchOverWholeNetwork = false;
return $scope.triggerSearch();
} else {

View File

@ -670,6 +670,19 @@ angular.module('application.router', ['ui.router'])
translations: ['Translations', function (Translations) { return Translations.query('app.admin.project_elements').$promise; }]
}
})
.state('app.admin.manage_abuses', {
url: '/admin/abuses',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/abuses/index.html" %>',
controller: 'AbusesController'
}
},
resolve: {
abusesPromise: ['Abuse', function(Abuse) { return Abuse.query().$promise; }],
translations: ['Translations', function(Translations) { return Translations.query('app.admin.manage_abuses').$promise; }]
}
})
// trainings
.state('app.admin.trainings', {

View File

@ -3,8 +3,8 @@
Application.Services.factory('Abuse', ['$resource', function ($resource) {
return $resource('/api/abuses/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
query: {
isArray: false
}
}
);

View File

@ -34,6 +34,7 @@
@import "app.plugins";
@import "modules/invoice";
@import "modules/signup";
@import "modules/abuses";
@import "app.responsive";

View File

@ -0,0 +1,31 @@
li.abuse {
list-style: none;
border: 1px solid #ddd;
border-radius: 2px;
margin-bottom: 2em;
.signaled {
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
padding: 1em;
position: relative;
button {
position: absolute;
right: 1em;
top: 0.5em;
}
}
.report {
padding: 2em;
cite {
display: block;
border-left: 4px solid #ddd;
padding-left: 1em;
margin-top: 1em;
}
}
}

View File

@ -0,0 +1,41 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'manage_abuses.abuses_list' }}</h1>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row m-b-md">
<span ng-show="abuses.length === 0" translate>{{ 'manage_abuses.no_reports' }}</span>
<ul ng-show="abuses.length > 0">
<li class="abuse" ng-repeat="abuse in abuses">
<div class="signaled">
<a ui-sref="app.public.projects_show({id:abuse.signaled.slug})">{{abuse.signaled.name}}</a>,
<span translate>{{ 'manage_abuses.published_by' }}</span>
<a ui-sref="app.admin.members_edit({id:abuse.signaled.author.id})">{{abuse.signaled.author.full_name}}</a>,
<span translate>{{ 'manage_abuses.at_date' }}</span>
<span>{{abuse.signaled.published_at | amDateFormat:'L' }}</span>
<button class="btn btn-success" ng-click="confirmProcess(abuse.id)">
<i class="fa fa-check"></i>
</button>
</div>
<div class="report">
<span translate>{{ 'manage_abuses.at_date' }}</span>
<span>{{abuse.created_at | amDateFormat:'L' }}</span>,
<a href="mailto:{{abuse.email}}">{{abuse.first_name}} {{abuse.last_name}}</a>
<span translate>{{ 'manage_abuses.has_reported' }}</span>
<cite>{{ abuse.message }}</cite>
</div>
</li>
</ul>
</div>
</section>

View File

@ -7,10 +7,14 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'projects_elements_management' }}</h1>
<h1 translate>{{ 'project_elements.projects_elements_management' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<a class="btn btn-ng btn-warning b-2x rounded m-t-sm upper text-sm" ui-sref="app.admin.manage_abuses" role="button" translate>{{ 'project_elements.manage_abuses' }}</a>
</section>
</div>
</div>
</section>
@ -26,7 +30,7 @@
<uib-tab heading="{{ 'themes' | translate }}">
<ng-include src="'<%= asset_path 'admin/project_elements/themes.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'licences' | translate }}">
<uib-tab heading="{{ 'project_elements.licences' | translate }}">
<ng-include src="'<%= asset_path 'admin/project_elements/licences.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -1,4 +1,4 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addLicence()" translate>{{ 'add_a_new_licence' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ng-click="addLicence()" translate>{{ 'project_elements.add_a_new_licence' }}</button>
<table class="table">
<thead>

View File

@ -1,4 +1,4 @@
<button type="button" class="btn btn-warning m-b m-t" ng-click="addComponent()" translate>{{ 'add_a_material' }}</button>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addComponent()" translate>{{ 'project_elements.add_a_material' }}</button>
<table class="table">
<thead>

View File

@ -1,4 +1,4 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTheme()" translate>{{ 'add_a_new_theme' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTheme()" translate>{{ 'project_elements.add_a_new_theme' }}</button>
<table class="table">
<thead>

View File

@ -7,13 +7,13 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'the_fablab_projects' }}</h1>
<h1 translate>{{ 'projects_list.the_fablab_projects' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin','member'])">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.logged.projects_new" role="button" translate>{{ 'add_a_project' }}</a>
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.logged.projects_new" role="button" translate>{{ 'projects_list.add_a_project' }}</a>
</section>
</div>
</div>
@ -23,10 +23,10 @@
<section class="m-lg">
<div class="row m-b-md">
<div class="col-md-12 m-b">
<a href="javascript:void(0);" class="text-sm pull-right" name="button" ng-click="resetFiltersAndTriggerSearch()" ng-show="!openlab.searchOverWholeNetwork"><i class="fa fa-refresh"></i> {{ 'reset_all_filters' | translate }}</a>
<a href="javascript:void(0);" class="text-sm pull-right" name="button" ng-click="resetFiltersAndTriggerSearch()" ng-show="!openlab.searchOverWholeNetwork"><i class="fa fa-refresh"></i> {{ 'projects_list.reset_all_filters' | translate }}</a>
<span ng-if="openlab.projectsActive" uib-tooltip="{{ 'tooltip_openlab_projects_switch' | translate }}" tooltip-trigger="mouseenter">
<label for="searchOverWholeNetwork" class="control-label m-r text-sm" translate>{{ 'search_over_the_whole_network' }}</label>
<span ng-if="openlab.projectsActive" uib-tooltip="{{ 'projects_list.tooltip_openlab_projects_switch' | translate }}" tooltip-trigger="mouseenter">
<label for="searchOverWholeNetwork" class="control-label m-r text-sm" translate>{{ 'projects_list.search_over_the_whole_network' }}</label>
<input bs-switch
ng-model="openlab.searchOverWholeNetwork"
type="checkbox"
@ -44,7 +44,7 @@
<div class="input-group-addon"><i class="fa fa-search"></i></div>
<input type="search" class="form-control" placeholder="Mots-clés" ng-model="search.q"/>
<div class="input-group-btn">
<button type="submit" class="btn btn-warning" translate>{{ 'search' }}</button>
<button type="submit" class="btn btn-warning" translate>{{ 'projects_list.search' }}</button>
</div>
</div>
</div>
@ -53,27 +53,27 @@
<span ng-if="!openlab.searchOverWholeNetwork">
<div class="col-md-3 m-b" ng-show="isAuthenticated()">
<select ng-model="search.from" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control">
<option value="" translate>{{ 'all_projects' }}</option>
<option value="mine" translate>{{ 'my_projects' }}</option>
<option value="collaboration" translate>{{ 'projects_to_whom_i_take_part_in' }}</option>
<option value="" translate>{{ 'projects_list.all_projects' }}</option>
<option value="mine" translate>{{ 'projects_list.my_projects' }}</option>
<option value="collaboration" translate>{{ 'projects_list.projects_to_whom_i_take_part_in' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.machine_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="m.id as m.name for m in machines">
<option value="" translate>{{ 'all_machines' }}</option>
<option value="" translate>{{ 'projects_list.all_machines' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.theme_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="t.id as t.name for t in themes">
<option value="" translate>{{ 'all_themes' }}</option>
<option value="" translate>{{ 'projects_list.all_themes' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.component_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="t.id as t.name for t in components">
<option value="" translate>{{ 'all_materials' }}</option>
<option value="" translate>{{ 'projects_list.all_materials' }}</option>
</select>
</div>
</span>
@ -81,7 +81,7 @@
<div class="row">
<span class="col-md-12" ng-show="projects && (projects.length == 0)"> {{ 'project_search_result_is_empty' | translate }} </span>
<span class="col-md-12" ng-show="projects && (projects.length == 0)"> {{ 'projects_list.project_search_result_is_empty' | translate }} </span>
<div class="col-xs-12 col-sm-6 col-md-3" ng-repeat="project in projects" ng-click="showProject(project)">
<div class="card card-project">
@ -99,7 +99,7 @@
</div>
<div class="text-center">
<span class="badge" ng-if="project.state == 'draft'" translate>{{ 'rough_draft' }}</span>
<span class="badge" ng-if="project.state == 'draft'" translate>{{ 'projects_list.rough_draft' }}</span>
</div>
<div class="card-overlay">
@ -119,7 +119,7 @@
<div class="row">
<div class="col-lg-12 text-center">
<a class="btn btn-warning" ng-click="loadMore()" ng-if="projectsPagination.hasNextPage()" translate>{{ 'load_next_projects' }}</a>
<a class="btn btn-warning" ng-click="loadMore()" ng-if="projectsPagination.hasNextPage()" translate>{{ 'projects_list.load_next_projects' }}</a>
</div>
</div>
</section>

View File

@ -4,6 +4,12 @@
# Typical action is an user reporting an abuse on a project
class API::AbusesController < API::ApiController
before_action :authenticate_user!, except: :create
before_action :set_abuse, only: %i[destroy]
def index
authorize Abuse
@abuses = Abuse.all
end
def create
@abuse = Abuse.new(abuse_params)
@ -14,8 +20,18 @@ class API::AbusesController < API::ApiController
end
end
def destroy
authorize Abuse
@abuse.destroy
head :no_content
end
private
def set_abuse
@abuse = Abuse.find(params[:id])
end
def abuse_params
params.require(:abuse).permit(:signaled_type, :signaled_id, :first_name, :last_name, :email, :message)
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Check the access policies for API::AbusesController
class AbusePolicy < ApplicationPolicy
def index?
user.admin?
end
def destroy?
user.admin?
end
end

View File

@ -0,0 +1,17 @@
json.abuses do
json.array!(@abuses) do |abuse|
json.extract! abuse, :id, :signaled_id, :signaled_type, :first_name, :last_name, :email, :message, :created_at
case abuse.signaled_type
when 'Project'
json.signaled do
json.extract! abuse.signaled, :name, :slug, :published_at
json.author do
json.id abuse.signaled.author.id
json.full_name abuse.signaled.author.profile.full_name
end
end
else
json.signaled abuse.signaled
end
end
end

View File

@ -63,7 +63,7 @@
<% end %>
<!-- RSS -->
<link rel="alternate" type="application/rss+xml" title="RSS: <%= t('app.public.projects_list.the_fablab_projects') %>" href="<%= rss_projects_path %>.xml">
<link rel="alternate" type="application/rss+xml" title="RSS: <%= t('app.public.projects_list.projects_list.the_fablab_projects') %>" href="<%= rss_projects_path %>.xml">
<link rel="alternate" type="application/rss+xml" title="RSS: <%= t('app.public.events_list.the_fablab_s_events') %>" href="<%= rss_events_path %>.xml">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->

View File

@ -64,11 +64,13 @@ en:
project_elements:
# management of the projects' components
project_elements:
projects_elements_management: "Projects elements management"
add_a_material: "Add a material"
add_a_new_theme: "Add a new theme"
licences: "Licences"
add_a_new_licence: "Add a new licence"
manage_abuses: "Manage the reports"
trainings:
# track and monitor the trainings
@ -754,3 +756,16 @@ en:
space_edit:
edit_the_space_NAME: "Edit the space: {{NAME}}" # angular interpolation
validate_the_changes: "Validate the changes"
manage_abuses:
# process and delete abuses reports
manage_abuses:
abuses_list: "Reports list"
no_reports: "No reports for now"
published_by: "published by"
at_date: "on"
has_reported: "made the following report:"
confirmation_required: "Confirm the processing of the report"
report_will_be_destroyed: "Once the report has been processed, it will be deleted. This can't be undone, continue?"
report_removed: "The report has been deleted"
failed_to_remove: "An error occurred, unable to delete the report"

View File

@ -64,11 +64,13 @@ es:
project_elements:
# management of the projects' components
project_elements:
projects_elements_management: "Gestión de elementos de proyectos"
add_a_material: "Añadir un material"
add_a_new_theme: "Añadir un nuevo tema"
licences: "Licencias"
add_a_new_licence: "Agregar una nueva licencia"
manage_abuses: "Administrar informes"
trainings:
# track and monitor the trainings
@ -754,3 +756,16 @@ es:
space_edit:
edit_the_space_NAME: "Edit the space: {{NAME}}" # angular interpolation # translation_missing
validate_the_changes: "Validar los cambios"
manage_abuses:
# process and delete abuses reports
manage_abuses:
abuses_list: "Lista de informes"
no_reports: "No informes por ahora"
published_by: "published by" # translation_missing
at_date: "on" # translation_missing
has_reported: "made the following report:" # translation_missing
confirmation_required: "Confirm the processing of the report" # translation_missing
report_will_be_destroyed: "Once the report has been processed, it will be deleted. This can't be undone, continue?" # translation_missing
report_removed: "The report has been deleted" # translation_missing
failed_to_remove: "An error occurred, unable to delete the report" # translation_missing

View File

@ -64,11 +64,13 @@ fr:
project_elements:
# gestion des éléments constituant les projets
project_elements:
projects_elements_management: "Gestion des éléments projets"
add_a_material: "Ajouter un matériau"
add_a_new_theme: "Ajouter une nouvelle thématique"
licences: "Licences"
add_a_new_licence: "Ajouter une nouvelle licence"
manage_abuses: "Gérer les signalements"
trainings:
# suivre et surveiller les formations
@ -754,3 +756,16 @@ fr:
space_edit:
edit_the_space_NAME: "Modifier l'espace : {{NAME}}" # angular interpolation
validate_the_changes: "Valider les modifications"
manage_abuses:
# traiter et supprimer les rapports d'abus
manage_abuses:
abuses_list: "Liste des signalements"
no_reports: "Aucun signalement pour le moment"
published_by: "publié par"
at_date: "le"
has_reported: "a effectué le signalement suivant:"
confirmation_required: "Confirmez le traitement du signalement"
report_will_be_destroyed: "Une fois le signalement traité, le rapport sera supprimé. Cette action est irréversible, continuer ?"
report_removed: "Le rapport a bien été supprimé"
failed_to_remove: "Une erreur est survenue, impossible de supprimer le rapport"

View File

@ -64,11 +64,13 @@ pt:
project_elements:
# management of the projects' components
project_elements:
projects_elements_management: "Gerenciar projetos e elementos"
add_a_material: "Adicionar um material"
add_a_new_theme: "Adicionar um novo tema"
licences: "Licenças"
add_a_new_licence: "Adicionar uma nova licença"
manage_abuses: "Gerenciar relatórios"
trainings:
# track and monitor the trainings
@ -754,3 +756,16 @@ pt:
space_edit:
edit_the_space_NAME: "Editar o espaço: {{NAME}}" # angular interpolation
validate_the_changes: "Validar mudanças"
manage_abuses:
# process and delete abuses reports
manage_abuses:
abuses_list: "Lista de relatórios"
no_reports: "Não há relatos de agora"
published_by: "published by" # translation_missing
at_date: "on" # translation_missing
has_reported: "made the following report:" # translation_missing
confirmation_required: "Confirm the processing of the report" # translation_missing
report_will_be_destroyed: "Once the report has been processed, it will be deleted. This can't be undone, continue?" # translation_missing
report_removed: "The report has been deleted" # translation_missing
failed_to_remove: "An error occurred, unable to delete the report" # translation_missing

View File

@ -148,7 +148,9 @@ en:
projects_list:
# projects gallery
projects_list:
the_fablab_projects: "The Fab Lab projects"
add_a_project: "Add a project"
search_over_the_whole_network: "Search over the whole Fab Manager network"
tooltip_openlab_projects_switch: "The search over the whole network lets you search over the projects of every Fab-manager using this feature !"
openlab_search_not_available_at_the_moment: "Search over the whole network is not available at the moment. You still can search over the projects of this platform."
@ -159,8 +161,10 @@ en:
my_projects: "My projects"
projects_to_whom_i_take_part_in: "Projects to whom I take part in"
all_machines: "All machines"
all_themes: "All themes"
all_materials: "All materials"
load_next_projects: "Load next projects"
rough_draft: "Rough draft"
projects_show:
# details of a projet

View File

@ -147,7 +147,9 @@ es:
projects_list:
# projects gallery
projects_list:
the_fablab_projects: "Los proyectos del FabLab"
add_a_project: "Añadir un proyecto"
search_over_the_whole_network: "Buscar en toda la red de FabLab"
tooltip_openlab_projects_switch: "La busqueda en toda la red le permite buscar los proyectos de todos los FabLab que usan esta característica"
openlab_search_not_available_at_the_moment: "La busqueda en toda la red no está disponible en este momento. Puede seguir buscando proyectos en este FabLab."
@ -158,8 +160,10 @@ es:
my_projects: "Mis proyectos"
projects_to_whom_i_take_part_in: "Proyectos de los que formo parte"
all_machines: "Todas las máquinas"
all_themes: "Todos los temas"
all_materials: "Todo el material"
load_next_projects: "Cargar más proyectos"
rough_draft: "Borrador"
projects_show:
# details of a projet

View File

@ -148,7 +148,9 @@ fr:
projects_list:
# galerie des projets
projects_list:
the_fablab_projects: "Les projets du FabLab"
add_a_project: "Ajouter un projet"
search_over_the_whole_network: "Chercher sur tout le réseau Fab Manager"
tooltip_openlab_projects_switch: "La recherche sur tout le réseau vous permet de rechercher parmis les projets de tous les Fab-managers utilisant cette fonctionnalité !"
openlab_search_not_available_at_the_moment: "La recherche sur tout le réseau n'est pas disponible pour le moment. Vous pouvez cependant effectuer une recherche parmis les projets de cette plateforme."
@ -159,8 +161,10 @@ fr:
my_projects: "Mes projets"
projects_to_whom_i_take_part_in: "Les projets auxquels je collabore"
all_machines: "Toutes les machines"
all_themes: "Toutes les thématiques"
all_materials: "Tous les matériaux"
load_next_projects: "Charger les projets suivants"
rough_draft: "Brouillon"
projects_show:
# détails d'un projet

View File

@ -148,7 +148,9 @@ pt:
projects_list:
# projects gallery
projects_list:
the_fablab_projects: "Projetos do Fab Lab"
add_a_project: "Adicionar projeto"
search_over_the_whole_network: "Pesquisar em todos os FabLabs"
tooltip_openlab_projects_switch: "A busca em todos os FabLabs busca projetos em todos os FabLabs que usam o Fab-manager !"
openlab_search_not_available_at_the_moment: "A busca em toda a rede de FabLabs não está disponível no momento. Você pode procurar por projetos nesta plataforma."
@ -159,8 +161,10 @@ pt:
my_projects: "Meus Projetos"
projects_to_whom_i_take_part_in: "Projetos que eu participo"
all_machines: "Todas as máquinas"
all_themes: "Todos temas"
all_materials: "Todos os materiais"
load_next_projects: "Carregar próximos projetos"
rough_draft: "Rascunho"
projects_show:
# details of a projet

View File

@ -123,7 +123,7 @@ Rails.application.routes.draw do
get 'active', action: 'active', on: :collection
post 'send_code', action: 'send_code', on: :collection
end
resources :abuses, only: [:create]
resources :abuses, only: %i[index create destroy]
resources :open_api_clients, only: %i[index create update destroy] do
patch :reset_token, on: :member
end