summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client/src/components/AdminLogin.tsx41
-rw-r--r--client/src/components/GuestLogin.tsx41
-rw-r--r--client/src/components/RsvpForm.tsx27
-rw-r--r--client/src/error.ts11
-rw-r--r--client/src/models.ts9
-rw-r--r--server/admin/handler.go30
-rw-r--r--server/guest/handler.go94
-rw-r--r--server/test/guest_test.go4
8 files changed, 191 insertions, 66 deletions
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) ? (
+ <Alert severity="error" onClose={() => setOpen(false)}>
+ {(error.data as Data).message}
+ </Alert>
+ ) : (
+ <Alert severity="error" onClose={() => setOpen(false)}>
+ Admin login failed
+ </Alert>
+ );
+};
function GuestLogin() {
const dispatch = useDispatch();
const navigate = useNavigate();
- const [login] = useLoginAdminMutation();
+ const [login, { isLoading, error }] = useLoginAdminMutation();
+ const [open, setOpen] = useState<boolean>(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
</Button>
</Paper>
+ <Snackbar
+ open={!isLoading && open}
+ onClose={() => setOpen(false)}
+ autoHideDuration={5000}
+ >
+ <div>
+ <Status {...({ error, setOpen } as StatusProps)} />
+ </div>
+ </Snackbar>
</form>
);
}
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) ? (
+ <Alert severity="error" onClose={() => setOpen(false)}>
+ {(error.data as Data).message}
+ </Alert>
+ ) : (
+ <Alert severity="error" onClose={() => setOpen(false)}>
+ Guest login failed
+ </Alert>
+ );
+};
function GuestLogin() {
const dispatch = useDispatch();
const navigate = useNavigate();
- const [login] = useLoginGuestMutation();
+ const [login, { isLoading, error }] = useLoginGuestMutation();
+ const [open, setOpen] = useState<boolean>(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
</Button>
</Paper>
+ <Snackbar
+ open={!isLoading && open}
+ onClose={() => setOpen(false)}
+ autoHideDuration={5000}
+ >
+ <div>
+ <Status {...({ error, setOpen } as StatusProps)} />
+ </div>
+ </Snackbar>
</form>
);
}
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 ? (
- <Alert severity="error" onClose={() => setOpen(false)}>
- RSVP failed
- </Alert>
+ isFetchBaseQueryError(error) ? (
+ <Alert severity="error" onClose={() => setOpen(false)}>
+ {(error.data as Data).message}
+ </Alert>
+ ) : (
+ <Alert severity="error" onClose={() => setOpen(false)}>
+ RSVP failed
+ </Alert>
+ )
) : (
<Alert severity="success" onClose={() => 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}
>
<div>
- <Status isError={isError} setOpen={setOpen} />
+ <Status {...({ isError, error, setOpen } as StatusProps)} />
</div>
</Snackbar>
</Grid>
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"))
}