From 46f925663e595fa8dd87f7aa15a0061abcfae3d7 Mon Sep 17 00:00:00 2001 From: Michael Hunteman Date: Mon, 2 Sep 2024 16:27:12 -0700 Subject: Add ui error messages --- client/src/components/AdminLogin.tsx | 41 ++++++++++++++-- client/src/components/GuestLogin.tsx | 41 ++++++++++++++-- client/src/components/RsvpForm.tsx | 27 ++++++----- client/src/error.ts | 11 +++++ client/src/models.ts | 9 ++++ server/admin/handler.go | 30 ++++++++---- server/guest/handler.go | 94 ++++++++++++++++++++++++------------ server/test/guest_test.go | 4 +- 8 files changed, 191 insertions(+), 66 deletions(-) create mode 100644 client/src/error.ts diff --git a/client/src/components/AdminLogin.tsx b/client/src/components/AdminLogin.tsx index 4f53566..4271d60 100644 --- a/client/src/components/AdminLogin.tsx +++ b/client/src/components/AdminLogin.tsx @@ -1,16 +1,38 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; -import { Button, Paper, TextField, Typography } from '@mui/material'; +import { + Alert, + Button, + Paper, + Snackbar, + TextField, + Typography, +} from '@mui/material'; import { useForm } from 'react-hook-form'; import { setAdmin } from '../slices/auth/adminSlice'; import { useLoginAdminMutation } from '../slices/api/adminSlice'; -import type { Credentials } from '../models'; +import type { Credentials, StatusProps } from '../models'; +import { isFetchBaseQueryError } from '../error'; +import type { Data } from '../error'; + +const Status = ({ error, setOpen }: StatusProps) => { + return isFetchBaseQueryError(error) ? ( + setOpen(false)}> + {(error.data as Data).message} + + ) : ( + setOpen(false)}> + Admin login failed + + ); +}; function GuestLogin() { const dispatch = useDispatch(); const navigate = useNavigate(); - const [login] = useLoginAdminMutation(); + const [login, { isLoading, error }] = useLoginAdminMutation(); + const [open, setOpen] = useState(false); const { register, @@ -28,7 +50,7 @@ function GuestLogin() { dispatch(setAdmin(await login(data).unwrap())); navigate('/dashboard'); } catch (e) { - console.log(e); + setOpen(true); } }; @@ -83,6 +105,15 @@ function GuestLogin() { Log in + setOpen(false)} + autoHideDuration={5000} + > +
+ +
+
); } diff --git a/client/src/components/GuestLogin.tsx b/client/src/components/GuestLogin.tsx index f42c1d6..acf229f 100644 --- a/client/src/components/GuestLogin.tsx +++ b/client/src/components/GuestLogin.tsx @@ -1,16 +1,38 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; -import { Button, Paper, TextField, Typography } from '@mui/material'; +import { + Alert, + Button, + Paper, + Snackbar, + TextField, + Typography, +} from '@mui/material'; import { useForm } from 'react-hook-form'; import { setGuest } from '../slices/auth/guestSlice'; import { useLoginGuestMutation } from '../slices/api/guestSlice'; -import type { Name } from '../models'; +import type { Name, StatusProps } from '../models'; +import { isFetchBaseQueryError } from '../error'; +import type { Data } from '../error'; + +const Status = ({ error, setOpen }: StatusProps) => { + return isFetchBaseQueryError(error) ? ( + setOpen(false)}> + {(error.data as Data).message} + + ) : ( + setOpen(false)}> + Guest login failed + + ); +}; function GuestLogin() { const dispatch = useDispatch(); const navigate = useNavigate(); - const [login] = useLoginGuestMutation(); + const [login, { isLoading, error }] = useLoginGuestMutation(); + const [open, setOpen] = useState(false); const { register, @@ -28,7 +50,7 @@ function GuestLogin() { dispatch(setGuest(await login(data).unwrap())); navigate('/rsvp'); } catch (e) { - console.log(e); + setOpen(true); } }; @@ -83,6 +105,15 @@ function GuestLogin() { Log in + setOpen(false)} + autoHideDuration={5000} + > +
+ +
+
); } diff --git a/client/src/components/RsvpForm.tsx b/client/src/components/RsvpForm.tsx index 33ca108..7b2892f 100644 --- a/client/src/components/RsvpForm.tsx +++ b/client/src/components/RsvpForm.tsx @@ -17,18 +17,21 @@ import { import MailIcon from '@mui/icons-material/Mail'; import { useForm, Controller, useFieldArray } from 'react-hook-form'; import { useUpdateGuestMutation } from '../slices/api/guestSlice'; -import type { Guest } from '../models'; +import type { Guest, StatusProps } from '../models'; +import { isFetchBaseQueryError } from '../error'; +import type { Data } from '../error'; -interface StatusProps { - isError: boolean; - setOpen: (open: boolean) => void; -} - -const Status = ({ isError, setOpen }: StatusProps) => { +const Status = ({ isError, error, setOpen }: StatusProps) => { return isError ? ( - setOpen(false)}> - RSVP failed - + isFetchBaseQueryError(error) ? ( + setOpen(false)}> + {(error.data as Data).message} + + ) : ( + setOpen(false)}> + RSVP failed + + ) ) : ( setOpen(false)}> RSVP updated @@ -37,7 +40,7 @@ const Status = ({ isError, setOpen }: StatusProps) => { }; function RsvpForm() { - const [updateGuest, { isLoading, isSuccess, isError }] = + const [updateGuest, { isLoading, isSuccess, isError, error }] = useUpdateGuestMutation(); const guest: Guest = useOutletContext(); const previousPartySize = useRef((guest.partySize ?? 1) - 1); @@ -278,7 +281,7 @@ function RsvpForm() { autoHideDuration={5000} >
- +
diff --git a/client/src/error.ts b/client/src/error.ts new file mode 100644 index 0000000..8935e23 --- /dev/null +++ b/client/src/error.ts @@ -0,0 +1,11 @@ +import type { FetchBaseQueryError } from '@reduxjs/toolkit/query/react'; + +export const isFetchBaseQueryError = ( + error: any +): error is FetchBaseQueryError => { + return 'data' in error && 'message' in error.data; +}; + +export interface Data { + message: string; +} diff --git a/client/src/models.ts b/client/src/models.ts index 6840a46..5e53484 100644 --- a/client/src/models.ts +++ b/client/src/models.ts @@ -1,3 +1,6 @@ +import { SerializedError } from '@reduxjs/toolkit'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; + export interface Guest { id?: number; firstName: string; @@ -28,3 +31,9 @@ export interface AdminLogin { guests: Guest[]; token: string; } + +export interface StatusProps { + isError?: boolean; + error: FetchBaseQueryError | SerializedError | undefined; + setOpen: (open: boolean) => void; +} diff --git a/server/admin/handler.go b/server/admin/handler.go index f2d5807..b05fc88 100644 --- a/server/admin/handler.go +++ b/server/admin/handler.go @@ -29,7 +29,8 @@ func NewAdminHandler(adminStore adminStore, guestStore guest.GuestStore) *AdminH return &AdminHandler{adminStore, guestStore} } -func (adminHandler *AdminHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { +func (adminHandler *AdminHandler) ServeHTTP(responseWriter http.ResponseWriter, + request *http.Request) { switch { case request.Method == http.MethodOptions: responseWriter.WriteHeader(http.StatusOK) @@ -40,7 +41,8 @@ func (adminHandler *AdminHandler) ServeHTTP(responseWriter http.ResponseWriter, } } -func (adminHandler *AdminHandler) handleLogIn(responseWriter http.ResponseWriter, request *http.Request) { +func (adminHandler *AdminHandler) handleLogIn(responseWriter http.ResponseWriter, + request *http.Request) { token, err := adminHandler.logIn(request) if err != nil { http.Error(responseWriter, err.Message, err.Code) @@ -52,29 +54,35 @@ func (adminHandler *AdminHandler) handleLogIn(responseWriter http.ResponseWriter func (adminHandler *AdminHandler) logIn(request *http.Request) ([]byte, *appError) { requestAdmin, err := adminHandler.decodeCredentials(request) if err != nil { - return []byte{}, &appError{err, "failed to unmarshal request", http.StatusBadRequest} + return []byte{}, &appError{err, "{ \"message\": \"Failed to unmarshal request\" }", + http.StatusBadRequest} } _, err = adminHandler.adminStore.Find(requestAdmin) if err != nil { - return []byte{}, &appError{err, "admin not found", http.StatusUnauthorized} + return []byte{}, &appError{err, "{ \"message\": \"Admin not found\" }", + http.StatusUnauthorized} } expirationTime := adminHandler.setExpirationTime() claims := adminHandler.createClaims(requestAdmin, expirationTime) key, err := adminHandler.readKey() if err != nil { - return []byte{}, &appError{err, "failed to read secret key", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to read secret key\" }", + http.StatusInternalServerError} } token, err := adminHandler.createToken(claims, key) if err != nil { - return []byte{}, &appError{err, "failed to create token", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to create token\" }", + http.StatusInternalServerError} } guests, err := adminHandler.guestStore.Get() if err != nil { - return []byte{}, &appError{err, "failed to get guests", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to get guests\" }", + http.StatusInternalServerError} } jsonBytes, err := adminHandler.marshalResponse(guests, token) if err != nil { - return []byte{}, &appError{err, "failed to marshal response", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to marshal response\" }", + http.StatusInternalServerError} } return jsonBytes, nil } @@ -109,11 +117,13 @@ func (adminHandler *AdminHandler) createToken(claims *Claims, key []byte) (strin return token.SignedString(key) } -func (adminHandler *AdminHandler) marshalResponse(guests []guest.Guest, token string) ([]byte, error) { +func (adminHandler *AdminHandler) marshalResponse(guests []guest.Guest, + token string) ([]byte, error) { loginResponse := adminHandler.createLoginResponse(guests, token) return json.Marshal(loginResponse) } -func (adminHandler *AdminHandler) createLoginResponse(guests []guest.Guest, token string) *Login { +func (adminHandler *AdminHandler) createLoginResponse(guests []guest.Guest, + token string) *Login { return &Login{guests, token} } diff --git a/server/guest/handler.go b/server/guest/handler.go index e4ba3f3..04adc0c 100644 --- a/server/guest/handler.go +++ b/server/guest/handler.go @@ -41,7 +41,8 @@ func NewGuestHandler(guestStore GuestStore) *GuestHandler { } } -func (handler *GuestHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { +func (handler *GuestHandler) ServeHTTP(responseWriter http.ResponseWriter, + request *http.Request) { switch { case request.Method == http.MethodOptions: responseWriter.WriteHeader(http.StatusOK) @@ -60,7 +61,8 @@ func (handler *GuestHandler) ServeHTTP(responseWriter http.ResponseWriter, reque } } -func (handler *GuestHandler) handleLogIn(responseWriter http.ResponseWriter, request *http.Request) { +func (handler *GuestHandler) handleLogIn(responseWriter http.ResponseWriter, + request *http.Request) { token, err := handler.logIn(request) if err != nil { http.Error(responseWriter, err.Message, err.Code) @@ -69,7 +71,8 @@ func (handler *GuestHandler) handleLogIn(responseWriter http.ResponseWriter, req } } -func (handler *GuestHandler) handlePut(responseWriter http.ResponseWriter, request *http.Request) { +func (handler *GuestHandler) handlePut(responseWriter http.ResponseWriter, + request *http.Request) { if err := handler.putGuest(request); err != nil { http.Error(responseWriter, err.Message, err.Code) } else { @@ -77,7 +80,8 @@ func (handler *GuestHandler) handlePut(responseWriter http.ResponseWriter, reque } } -func (handler *GuestHandler) handleGet(responseWriter http.ResponseWriter, request *http.Request) { +func (handler *GuestHandler) handleGet(responseWriter http.ResponseWriter, + request *http.Request) { guests, err := handler.getGuests(request) if err != nil { http.Error(responseWriter, err.Message, err.Code) @@ -86,7 +90,8 @@ func (handler *GuestHandler) handleGet(responseWriter http.ResponseWriter, reque } } -func (handler *GuestHandler) handlePost(responseWriter http.ResponseWriter, request *http.Request) { +func (handler *GuestHandler) handlePost(responseWriter http.ResponseWriter, + request *http.Request) { if err := handler.postGuest(request); err != nil { http.Error(responseWriter, err.Message, err.Code) } else { @@ -94,7 +99,8 @@ func (handler *GuestHandler) handlePost(responseWriter http.ResponseWriter, requ } } -func (handler *GuestHandler) handleDelete(responseWriter http.ResponseWriter, request *http.Request) { +func (handler *GuestHandler) handleDelete(responseWriter http.ResponseWriter, + request *http.Request) { if err := handler.deleteGuest(request); err != nil { http.Error(responseWriter, err.Message, err.Code) } else { @@ -105,25 +111,30 @@ func (handler *GuestHandler) handleDelete(responseWriter http.ResponseWriter, re func (handler *GuestHandler) logIn(request *http.Request) ([]byte, *appError) { name, err := handler.decodeName(request) if err != nil { - return []byte{}, &appError{err, "failed to unmarshal name", http.StatusBadRequest} + return []byte{}, &appError{err, "{ \" message\": \"Failed to unmarshal name\" }", + http.StatusBadRequest} } guest, err := handler.store.Find(name) if err != nil { - return []byte{}, &appError{err, "guest not found", http.StatusUnauthorized} + return []byte{}, &appError{err, "{ \"message\": \"Guest not found\" }", + http.StatusUnauthorized} } expirationTime := handler.setExpirationTime() claims := handler.createClaims(name, expirationTime) key, err := handler.readGuestKey() if err != nil { - return []byte{}, &appError{err, "failed to read secret key", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to read secret key\" }", + http.StatusInternalServerError} } token, err := handler.createToken(claims, key) if err != nil { - return []byte{}, &appError{err, "failed to create token", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to create token\" }", + http.StatusInternalServerError} } jsonBytes, err := handler.marshalResponse(guest, token) if err != nil { - return []byte{}, &appError{err, "failed to marshal response", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to marshal response\" }", + http.StatusInternalServerError} } return jsonBytes, nil } @@ -177,20 +188,24 @@ func (handler *GuestHandler) createLoginResponse(weddingGuest Guest, token strin func (handler *GuestHandler) putGuest(request *http.Request) *appError { guestKey, err := handler.readGuestKey() if err != nil { - return &appError{err, "failed to read secret key", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to read secret key\" }", + http.StatusInternalServerError} } if err := handler.validateToken(request, guestKey); err != nil { return err } if handler.findID(request) { - return &appError{errors.New("id not found"), "id not found", http.StatusNotFound} + return &appError{errors.New("ID not found"), "{ \"message\": \"ID not found\" }", + http.StatusNotFound} } guest, err := handler.decodeGuest(request) if err != nil { - return &appError{err, "invalid guest", http.StatusBadRequest} + return &appError{err, "{ \"message\": \"Invalid guest\" }", + http.StatusBadRequest} } if err := handler.store.Update(guest); err != nil { - return &appError{err, "failed to update guest", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to update guest\" }", + http.StatusInternalServerError} } return nil } @@ -201,12 +216,15 @@ func (handler *GuestHandler) validateToken(request *http.Request, key []byte) *a token, err := handler.parseWithClaims(authorizationHeader, claims, key) if err != nil { if err == jwt.ErrSignatureInvalid { - return &appError{err, "invalid signature", http.StatusUnauthorized} + return &appError{err, "{ \"message\": \"Invalid signature\" }", + http.StatusUnauthorized} } - return &appError{err, "failed to parse claims", http.StatusBadRequest} + return &appError{err, "{ \"message\": \"Failed to parse claims\" }", + http.StatusBadRequest} } if !token.Valid { - return &appError{err, "invalid token", http.StatusUnauthorized} + return &appError{err, "{ \"message\": \"Invalid token\" }", + http.StatusUnauthorized} } return nil } @@ -219,7 +237,8 @@ func (handler *GuestHandler) newClaims() *Claims { return &Claims{} } -func (handler *GuestHandler) parseWithClaims(token string, claims *Claims, key []byte) (*jwt.Token, error) { +func (handler *GuestHandler) parseWithClaims(token string, claims *Claims, + key []byte) (*jwt.Token, error) { return jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) { return key, nil }) @@ -240,18 +259,21 @@ func (handler *GuestHandler) decodeGuest(request *http.Request) (Guest, error) { func (handler *GuestHandler) getGuests(request *http.Request) ([]byte, *appError) { adminKey, err := handler.readAdminKey() if err != nil { - return []byte{}, &appError{err, "failed to read secret key", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to read secret key\" }", + http.StatusInternalServerError} } if err := handler.validateToken(request, adminKey); err != nil { return []byte{}, err } guests, err := handler.store.Get() if err != nil { - return []byte{}, &appError{err, "failed to get guests", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to get guests\" }", + http.StatusInternalServerError} } jsonBytes, err := json.Marshal(guests) if err != nil { - return []byte{}, &appError{err, "failed to marshal guests", http.StatusInternalServerError} + return []byte{}, &appError{err, "{ \"message\": \"Failed to marshal guests\" }", + http.StatusInternalServerError} } return jsonBytes, nil } @@ -259,24 +281,29 @@ func (handler *GuestHandler) getGuests(request *http.Request) ([]byte, *appError func (handler *GuestHandler) postGuest(request *http.Request) *appError { adminKey, err := handler.readAdminKey() if err != nil { - return &appError{err, "failed to read secret key", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to read secret key\" }", + http.StatusInternalServerError} } if err := handler.validateToken(request, adminKey); err != nil { return err } guest, err := handler.decodeGuest(request) if err != nil { - return &appError{err, "invalid guest", http.StatusBadRequest} + return &appError{err, "{ \"message\": \"Invalid guest\" }", + http.StatusBadRequest} } guests, err := handler.store.Get() if err != nil { - return &appError{err, "failed to get guests", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to get guests\" }", + http.StatusInternalServerError} } if err := handler.checkExistingGuests(guests, guest); err != nil { - return &appError{err, "id already exists", http.StatusConflict} + return &appError{err, "{ \"message\": \"ID already exists\" }", + http.StatusConflict} } if err := handler.store.Add(guest); err != nil { - return &appError{err, "failed to add guest", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to add guest\" }", + http.StatusInternalServerError} } return nil } @@ -284,7 +311,7 @@ func (handler *GuestHandler) postGuest(request *http.Request) *appError { func (handler *GuestHandler) checkExistingGuests(guests []Guest, newGuest Guest) error { for _, guest := range guests { if guest.ID == newGuest.ID { - return errors.New("id already exists") + return errors.New("ID already exists") } } return nil @@ -293,21 +320,24 @@ func (handler *GuestHandler) checkExistingGuests(guests []Guest, newGuest Guest) func (handler *GuestHandler) deleteGuest(request *http.Request) *appError { adminKey, err := handler.readAdminKey() if err != nil { - return &appError{err, "failed to read secret key", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to read secret key\" }", + http.StatusInternalServerError} } if err := handler.validateToken(request, adminKey); err != nil { return err } if handler.findID(request) { - return &appError{errors.New("id not found"), "id not found", http.StatusNotFound} + return &appError{errors.New("ID not found"), "ID not found", http.StatusNotFound} } guestID, err := getID(request) if err != nil { - return &appError{err, "failed to parse id", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to parse ID\" }", + http.StatusInternalServerError} } err = handler.store.Delete(int(guestID)) if err != nil { - return &appError{err, "failed to get guests", http.StatusInternalServerError} + return &appError{err, "{ \"message\": \"Failed to get guests\" }", + http.StatusInternalServerError} } return nil } diff --git a/server/test/guest_test.go b/server/test/guest_test.go index 5bb77ff..490df84 100644 --- a/server/test/guest_test.go +++ b/server/test/guest_test.go @@ -154,11 +154,11 @@ func assertEquals(test testing.TB, actual any, expected any) { } func getName() string { - return "{ \"firstName\": \"Michael\", \"lastName\": \"Hunteman\"}" + return "{ \"firstName\": \"Michael\", \"lastName\": \"Hunteman\" }" } func getCredentials() string { - return fmt.Sprintf("{ \"username\": \"mhunteman\", \"password\": \"%s\"}", + return fmt.Sprintf("{ \"username\": \"mhunteman\", \"password\": \"%s\" }", os.Getenv("PASS")) } -- cgit v1.2.3