Skip to content

Commit

Permalink
feat: add update employee password (#213)
Browse files Browse the repository at this point in the history
Refs: closes #211

## Summary

Add update employee password feature.
  • Loading branch information
joaotomaspinheiro authored May 26, 2024
1 parent 1aa3f04 commit 299bf83
Show file tree
Hide file tree
Showing 16 changed files with 534 additions and 23 deletions.
1 change: 1 addition & 0 deletions web/src/domain/components/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Size of an icon.
*/
export type IconSize =
| "xx-small"
| "x-small"
| "small"
| "medium"
Expand Down
27 changes: 24 additions & 3 deletions web/src/lib/clients/ecomap/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import type { components, paths } from "../../../../api/ecomap/http";
import { clearToken, getToken } from "../../utils/auth";
import { CommonRoutes } from "../../../routes/constants/routes";

/**
* Endpoints that should be ignored if the status code from the server response is 401.
* Used to prevent the employee token from being deleted and the employee from being redirected to the sign in page.
*/
const UNAUTHORIZED_IGNORED_ENDPOINTS: (keyof paths)[] = ["/employees/password"];

const middleware: Middleware = {
onRequest(request) {
const token = getToken();
Expand All @@ -13,17 +19,31 @@ const middleware: Middleware = {

return request;
},
async onResponse(response) {
let body = await response.text();
async onResponse(response, options) {
let body = (await response.text()) || null;

switch (response.status) {
case 401:
case 401: {
const responseUrl = new URL(response.url);

// Check if it's an endpoint that should be ignored.
const isIgnoredEndpoint = UNAUTHORIZED_IGNORED_ENDPOINTS.some(
endpoint => {
const pathname = `${options.baseUrl}${endpoint}`;
return pathname === responseUrl.pathname;
},
);
if (isIgnoredEndpoint) {
break;
}

clearToken();
// Only redirect if the page is not the sign in page.
if (location.pathname !== CommonRoutes.SIGN_IN) {
navigate(CommonRoutes.SIGN_IN);
}
break;
}

case 403:
navigate(CommonRoutes.FORBIDDEN, { replace: true });
Expand All @@ -44,6 +64,7 @@ const middleware: Middleware = {
const ecomapHttpClient = createClient<paths>({
baseUrl: "/api",
});

ecomapHttpClient.use(middleware);

export default ecomapHttpClient;
26 changes: 24 additions & 2 deletions web/src/lib/components/FormControl.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import Icon from "./Icon.svelte";
/**
* A space-separated list of the classes of the element.
* @default ""
Expand All @@ -23,11 +25,22 @@
* @default null
*/
export let label: string | null = null;
/**
* The text representing advisory information related to the element it belongs to.
* @default null
*/
export let title: string | null = null;
</script>

<label class={className}>
{#if label}
{label}
<span {title} class="header">
<span class="input-label">{label}</span>
{#if title}
<Icon name="info" size="xx-small" />
{/if}
</span>
{/if}
<slot />
{#if helperText}
Expand All @@ -39,7 +52,16 @@
label {
display: flex;
flex-direction: column;
color: var(--gray-500);
}
.header {
display: flex;
align-items: center;
gap: 0.25rem;
& .input-label {
color: var(--gray-500);
}
}
.helper-text {
Expand Down
52 changes: 52 additions & 0 deletions web/src/lib/components/FormModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts">
import type { FormEventHandler } from "svelte/elements";
import Modal from "./Modal.svelte";
/**
* Indicates if modal is open.
*/
export let open: boolean;
/**
* The modal title.
*/
export let title: string;
/**
* Callback fired when the form is submitted.
*/
export let onSubmit: FormEventHandler<HTMLFormElement>;
/**
* Callback fired when open state changes.
* @param open Modal open state.
*/
export let onOpenChange: (open: boolean) => void;
/**
* Indicates whether the modal content is automatically configured with gutters.
* @default false
*/
export let gutters: boolean = false;
/**
* The form element.
* @default null
*/
export let form: HTMLFormElement | null = null;
/**
* Callback fired when backdrop is clicked.
* @default null
*/
export let onBackdropClick: (() => void) | null = null;
</script>

<form novalidate bind:this={form} on:submit|preventDefault={onSubmit}>
<Modal {open} {gutters} {title} {onOpenChange} {onBackdropClick}>
<slot />
<svelte:fragment slot="actions">
<slot name="actions" />
</svelte:fragment>
</Modal>
</form>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import IconButton from "../components/IconButton.svelte";
import IconButton from "./IconButton.svelte";
/**
* Indicates if modal is open.
Expand Down Expand Up @@ -28,7 +28,7 @@
* Callback fired when backdrop is clicked.
* @default null
*/
export let onClickOutside: (() => void) | null = null;
export let onBackdropClick: (() => void) | null = null;
/**
* Dialog element.
Expand All @@ -51,7 +51,7 @@
// Check if the element that was pressed is the dialog. If it is, it means that the click was performed outside the modal.
if (e.target === dialog) {
closeModal();
onClickOutside?.();
onBackdropClick?.();
}
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/lib/components/SelectLocation.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import OlMap from "ol/Map";
import Modal from "../clients/Modal.svelte";
import Modal from "./Modal.svelte";
import Map from "./map/Map.svelte";
import VectorLayer from "ol/layer/Vector";
import { Feature, MapBrowserEvent } from "ol";
Expand Down Expand Up @@ -256,7 +256,7 @@
<Modal
{open}
{onOpenChange}
onClickOutside={removeSelectedLocation}
onBackdropClick={removeSelectedLocation}
title={$t("selectLocation")}
>
<Map bind:map mapId="select-location-map" --height="32rem" --width="60rem" />
Expand Down
17 changes: 16 additions & 1 deletion web/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,20 @@
"employees.notFound.description": "The employee you are looking for does not exist.",
"employees.delete.success": "Employee deleted successfully",
"employees.delete.conflict.title": "Conflict",
"employees.delete.conflict.description": "This employee cannot be deleted because it's associated with a route."
"employees.delete.conflict.description": "This employee cannot be deleted because it's associated with a route.",
"employees.updatePassword.title": "Change password",
"employees.updatePassword.currentPassword.label": "Current password",
"employees.updatePassword.newPassword.label": "New password",
"employees.updatePassword.confirmPassword.label": "Confirm password",
"employees.updatePassword.currentPassword.placeholder": "Enter the current password",
"employees.updatePassword.newPassword.placeholder": "Enter the new password",
"employees.updatePassword.confirmPassword.placeholder": "Repeat the new password",
"employees.updatePassword.success": "Password changed successfully",
"employees.updatePassword.error.passwordConstraints.title": "Invalid password",
"employees.updatePassword.error.passwordConstraints.description": "The password must contain at least 14 characters, including one regular character, one special character, and one number",
"employees.updatePassword.error.incorrectPassword.title": "Incorrect credentials",
"employees.updatePassword.error.incorrectPassword.description": "The password entered does not match your current password.",
"employees.error.passwordMismatch": "The password doesn't match the new password",
"employees.passwordConstraints": "Password must contain at least 14 characters, including one regular character, one special character, and one number",
"confirm": "Confirm"
}
17 changes: 16 additions & 1 deletion web/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,20 @@
"employees.notFound.description": "O colaborador que procura não existe.",
"employees.delete.success": "Colaborador eliminado com sucesso",
"employees.delete.conflict.title": "Conflito",
"employees.delete.conflict.description": "Este colaborador não pode ser eliminado porque está associado a uma rota."
"employees.delete.conflict.description": "Este colaborador não pode ser eliminado porque está associado a uma rota.",
"employees.updatePassword.title": "Alterar palavra-passe",
"employees.updatePassword.currentPassword.label": "Palavra-passe atual",
"employees.updatePassword.newPassword.label": "Nova palavra-passe",
"employees.updatePassword.confirmPassword.label": "Confirmar palavra-passe",
"employees.updatePassword.currentPassword.placeholder": "Introduza a palavra-passe atual",
"employees.updatePassword.newPassword.placeholder": "Introduza a nova palavra-passe",
"employees.updatePassword.confirmPassword.placeholder": "Repita a nova palavra-passe",
"employees.updatePassword.success": "Palavra-passe alterada com sucesso",
"employees.updatePassword.error.passwordConstraints.title": "Palavra-passe inválida",
"employees.updatePassword.error.passwordConstraints.description": "A palavra-passe deve conter pelo menos 14 caracteres, incluindo um carácter regular, um carácter especial e um número.",
"employees.updatePassword.error.incorrectPassword.title": "Credenciais inválidas",
"employees.updatePassword.error.incorrectPassword.description": "A palavra-passe introduzida não corresponde à sua palavra-passe atual.",
"employees.error.passwordMismatch": "A palavra-passe não corresponde à nova palavra-passe",
"employees.passwordConstraints": "A palavra-passe deve conter pelo menos 14 caracteres, incluindo um carácter regular, um carácter especial e um número",
"confirm": "Confirmar"
}
45 changes: 45 additions & 0 deletions web/src/locales/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,51 @@
},
"employees.delete.conflict.description": {
"type": "string"
},
"employees.updatePassword.title": {
"type": "string"
},
"employees.updatePassword.currentPassword.label": {
"type": "string"
},
"employees.updatePassword.newPassword.label": {
"type": "string"
},
"employees.updatePassword.confirmPassword.label": {
"type": "string"
},
"employees.updatePassword.currentPassword.placeholder": {
"type": "string"
},
"employees.updatePassword.newPassword.placeholder": {
"type": "string"
},
"employees.updatePassword.confirmPassword.placeholder": {
"type": "string"
},
"employees.updatePassword.success": {
"type": "string"
},
"employees.updatePassword.error.passwordConstraints.title": {
"type": "string"
},
"employees.updatePassword.error.passwordConstraints.description": {
"type": "string"
},
"employees.updatePassword.error.incorrectPassword.title": {
"type": "string"
},
"employees.updatePassword.error.incorrectPassword.description": {
"type": "string"
},
"employees.error.passwordMismatch": {
"type": "string"
},
"employees.passwordConstraints": {
"type": "string"
},
"confirm": {
"type": "string"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@
* Handles the submit event of the form.
* @param e Submit event.
*/
function handleSubmit(e: SubmitEvent) {
const formData = new FormData(e.currentTarget as HTMLFormElement);
function handleSubmit(
e: Event & { currentTarget: EventTarget & HTMLFormElement },
) {
const formData = new FormData(e.currentTarget);
const category = formData.get("category") ?? "";
const location = formData.get("location") ?? "";
Expand Down
24 changes: 23 additions & 1 deletion web/src/routes/backOffice/employees/details/Employee.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,24 @@
import { getToastContext } from "../../../../lib/contexts/toast";
import { BackOfficeRoutes } from "../../../constants/routes";
import { isViewingSelf } from "../../../../lib/utils/auth";
import UpdatePasswordModal from "./UpdatePasswordModal.svelte";
/**
* Employee ID.
*/
export let id: string;
/**
* Toast context.
*/
const toast = getToastContext();
/**
* The update password open modal state.
* @default false
*/
let openUpdatePasswordModal = false;
/**
* Fetches employee data.
*/
Expand Down Expand Up @@ -92,7 +102,14 @@
)}

<DetailsHeader to="" title={`${employee.firstName} ${employee.lastName}`}>
{#if !isViewingSelf(employee.id)}
{#if isViewingSelf(employee.id)}
<Button
variant="secondary"
onClick={() => (openUpdatePasswordModal = true)}
>
{$t("employees.updatePassword.title")}
</Button>
{:else}
<Button
startIcon="delete"
actionType="danger"
Expand Down Expand Up @@ -144,6 +161,11 @@
</DetailsFields>
</DetailsSection>
</DetailsContent>
<UpdatePasswordModal
{employee}
open={openUpdatePasswordModal}
onOpenChange={open => (openUpdatePasswordModal = open)}
/>
{:catch}
<div class="employee-not-found">
<h2>{$t("employees.notFound.title")}</h2>
Expand Down
Loading

0 comments on commit 299bf83

Please sign in to comment.