Aplikacje webowa pozwalająca zalogowanym użytkownikom na wprowadzanie pomiarów ciśnienia krwi w danym dniu i o konkretnej porze dnia. Aplikacja została stworzona z w sposób umożliwiający zapisywanie danych pomimo braku połączenia z internetem (w trybie offline przeglądarki, który należy ręcznie włączyć). Po ponownym nawiązaniu połączenia (powrotu do trybu online) dane są automatyczne synchronizowane ze zdalną bazą danych. Istotnym aspektem jest wymóg bycia wcześniej zalogowanym aby móc działać w trybie offline - wymagana jest obecność tokenu uwierzytelnienia w localStorage.
Przykładowe konto użytkownika:
email: example7@email.com
hasło: password
Tryb offline zgodnie z wymaganiami określonymi w poleceniu rozumiany jest jako ręczne włączenie takiego trybu w przegladarce - mechanizm przełączania trybów oparty jest o eventy online/offline oraz zmienną navigator.onLine, które nie do końca i nie we wszytskich przeglądarkach radzą sobie z wykryciem np. odłączenia kabla.
Część backendowa aplikacji znajduje się w osobnym repozytorium.
-
Frontend - React + Typescript
-
Backend - Express + Typescript
-
Baza danych - MongoDB
-
chartjs - biblioteka do tworzenia wykresów
-
react-hook-form - obsługa formularzy z walidacją po stronie klienta
-
material-ui - biblioteka gotowych komponentów Reactowych (wykorzystana na najniższym poziomie abstrakcji - przyciski, pola formularzy, date picker)
-
styled-components
-
react-router-dom
Serwerowa część aplikacji umieszczona została na platformie AWS w usłudze Elastic Beanstalk. Frontend aplikacji serwowany jest z usługi S3 jako static web hosting. Baza MongoDB hostowana jest w oficjalnej usłudze chmurowej MongoDB Atlas.
Struktura API zaprojektowana została zgodnie z regułami REST - api jest bezstanowe a konkretne zasoby zdefiniowane są poprzez url oraz metodę HTTP. Uwierzytelnienie oparte jest o mechanizm JWT.
Dostępne endpointy:
-
POST /auth/login
-
POST /auth/register
-
POST /measurements - dodanie nowego pomiaru dla użytkownika (chroniony)
-
POST /measurements/sync -synchronizacja danych lokalnych ze zdalną bazą danych (chroniony)
-
GET /measurements - pobranie wszystkich pomiarów ciśnienia krwi użytkownika (chroniony)
-
DELETE /measurements/:id - usunięcie konkretnego pomiary o id zawartym jako url param (chroniony)
Obsługa uwierzytelniania użytkowników zaimplementowana została z wykorzystaniem technologii JWT (JSON Web Token). Podpisany przez serwer token przechowywany jest w localStorage i dołączany jest do każdego zapytania zalogowanego użytkownika w celu potwierdzenia jego tożsamości. Po stronie serwera za sprawdzanie tożsamości klientów i odrzucanie nieuprawnionych użytkowników odpowiedzialny jest middleware authUser który dołączany jest do strzeżonych endpointów REST API.
Mechanizm obsługi trybów online/offline zaprojektowany został zgodnie z zasadą ostatnią zasadą SOLID czyli dependency inversion poprzez mechanizm dependency injection. tym celu zdefiniowany został interfejs biznesowy MeasurementService określający akcje, które użytkownik może wykonać na rzecz swoich danych:
export interface MeasurementService {
addOne(measurement: Measurement): Promise<Measurement>;
fetchAll(): Promise<Measurement[]>;
deleteOne(id: string): Promise<string>;
}
Ten interfejs jest implementowany przed dwa serwisy - MeasurementOfflineService oraz MeasurementOnlineService przy czym serwis służący do pracy offline zamiast komunikacji ze zdalnym serwerem, który zapisuje dane w bazie danych, oparty jest na lokalnym mechanizmie wbudowanym w przeglądarki - localStorage. Kluczowy moment zachodzi w Komponencie wyższego rzędu (HOC) NetworkDetector, który opakowuje właściwą aplikację Providerem kontekstu reactowego (React Context API) z wstrzykniętym odpowiednim serwisem w zależności od sytuacji:
return (
<AuthProvider>
<MeasurementProvider
measurementService={
isOnline
? measurementOnlineService
: measurementOfflineService}
>
<App/>
</MeasurementProvider>
</AuthProvider>
)
Z racji identycznego interfejsu obu serwisów ani sam kontekst ani żadna inna część aplikacji nie musi i nie wie w jakim trybie się znajduje i wywołuje zawsze te same metody, które dopiero na najniższym poziomie abstrakcji czyli w konkretnej implementacji serwisu wykazują się specyficznym dla trybu działaniem. W efekcie użytkownik nie odczuwa przejścia pomiędzy trybami lub nie widzi różnicy w korzystaniu z aplikacji w trybie offline (z dokładnością do braku danych z bazy w chmurze w trybie offline).
Po stronie klienta poprawność danych sprawdzana jest przy pomocy biblioteki react-hook-form, która sprawdza zdefiniowane przez nas warunki dla każdego z pól formularza i zwraca odpowiednie komunikaty, które następnie w prosty sposób możemy wyświetlić.
const inputRegister = {
firstName: register({
required: 'First name is required',
minLength: { value: 2, message: 'Minimum 2 characters' },
maxLength: { value: 16, message: 'Maximum 16 characters' },
}),
lastName: register({
required: 'Last name is required',
minLength: { value: 2, message: 'Minimum 2 characters' },
maxLength: { value: 16, message: 'Maximum 16 characters' },
}),
email: register({
required: 'Email is required',
minLength: { value: 4, message: 'Minimum 4 characters' },
pattern: { value: /(.+)@(.+){2,}\.(.+){2,}/, message: 'Invalid email format'}
}),
password: register({
required: 'Password is required',
minLength: { value: 8, message: 'Minimum 8 characters' },
maxLength: { value: 16, message: 'Maximum 16 characters' },
})
}
Warto nadmienić, iż użyta biblioteka cechuje się świetną wydajnością (najmniejsza ilość przerenderowań komponentów spośród czołówki bibliotek obsługujących formularze w React) z racji wykorzystania referencji.
Tutaj poprawności danych strzeże duet dwóch bibliotek - class-transformer i class-validator działający w middleware validateBodyAs. Reguły poprawności danych definiujemy poprzez odpowiednie dekoratory dołączane do pól obiektów transferu danych (DTO - Data Transfer Object).
export default class AuthCredentialsDto {
@Expose()
@IsString({ message: 'missing email' })
public email: string;
@Expose()
@IsString({ message: 'missing password' })
public password: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}
Spodziewając się konkretnej struktury danych w body zapytania poprzedzamy handler middlewarem z przekazanym odpowiednym typem DTO:
this.router.post('/login', validateBodyAs(AuthCredentialsDto), this.signIn);
W środku zawartość body pochodząca od klienta transformowana jest na ten obiekt DTO i następnie podawana jest walidacji. W przypadku niepowodzenia, tworzony jest zbiorczy komunikat, który zostaje wysłany w odpowiedzi z kodem 400 (Bad Request)
W tym miejscu użyte zostały wzorce repozytorium oraz singletonu - każde z dwóch repozytoriów zaimplementowane jest jak singleton aby jego instancja tworzona była tylko raz i była współdzielona przez kontrolery. Repozytoria mają zaimplementowane metody pozwalające na podstawowe operacje na danych zasobach. Kontrolery nie wiedzą nic o bazie danych ani nie mają do niej bezpośredniego dostępu a jedynie wywołują odpowiednie metody z używanych repozytoriów przekazując do nich niezbędne informacje takie jak obiekty DTO czy identyfikatory użytkowników.
Kod aplikacji klienta został skompilowany do standardu ES5 a więc wszystkie wersje przeglądarek wspierające ten standard powinny prawidłowo obsługiwać aplikację. Naturalnie wymogiem działania aplikacji jest obsługa języka JavaScript.
Do pracy w trybie offline zalecana jest przeglądarka Mozilla Firefox, która domyślnie cachuje odwiedzone strony i wczytuje je bezproblemowo w momencie włączenia trybu offline.