summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Hunteman <michael@huntm.net>2024-06-23 13:55:42 -0700
committerMichael Hunteman <michael@huntm.net>2024-06-23 13:55:42 -0700
commit07752babb4e692452e1cd7f2133c4d8dde1b3b1c (patch)
treeb3be7698f1af43f83bccd3bbbf6e19cd03532f1b
parent4bf5d1a620dfe96ea9593d44cfcd0f142fcdec61 (diff)
Authenticate UI users
-rw-r--r--client/src/apiSlice.ts10
-rw-r--r--client/src/components/Rsvp.tsx2
-rw-r--r--client/src/components/RsvpForm.tsx17
-rw-r--r--client/src/features/auth/authSlice.ts9
-rw-r--r--client/src/main.tsx27
-rw-r--r--client/src/mocks/handlers.ts2
-rw-r--r--client/src/pages.ts2
-rw-r--r--server/cmd/main.go83
-rw-r--r--server/go.mod10
-rw-r--r--server/go.sum14
-rw-r--r--server/guests/models.go17
-rw-r--r--server/guests/store.go31
12 files changed, 169 insertions, 55 deletions
diff --git a/client/src/apiSlice.ts b/client/src/apiSlice.ts
index 5d987f9..7842da8 100644
--- a/client/src/apiSlice.ts
+++ b/client/src/apiSlice.ts
@@ -30,7 +30,7 @@ export interface PartyGuest {
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
- baseUrl: '/',
+ baseUrl: 'http://localhost:8080/',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
@@ -42,20 +42,20 @@ export const apiSlice = createApi({
tagTypes: ['Guests'],
endpoints: (builder) => ({
getGuests: builder.query<void, void>({
- query: () => '/guests',
+ query: () => 'guests',
providesTags: ['Guests'],
}),
updateGuest: builder.mutation<Guest, Guest>({
query: (guest) => ({
- url: `/guests/${guest?.id}`,
- method: 'PATCH',
+ url: `guests/${guest?.id}`,
+ method: 'PUT',
body: guest,
providesTags: ['Guests'],
}),
}),
login: builder.mutation<LoginResponse, LoginRequest>({
query: (credentials) => ({
- url: '/guest-login',
+ url: 'guests/login',
method: 'POST',
body: credentials,
}),
diff --git a/client/src/components/Rsvp.tsx b/client/src/components/Rsvp.tsx
index dad7213..fbcaf4a 100644
--- a/client/src/components/Rsvp.tsx
+++ b/client/src/components/Rsvp.tsx
@@ -22,7 +22,7 @@ function Rsvp() {
<Outlet context={auth?.guest} />
</>
) : (
- <Navigate to="/guest-login" state={{ from: location }} replace />
+ <Navigate to="/guests/login" state={{ from: location }} replace />
);
}
diff --git a/client/src/components/RsvpForm.tsx b/client/src/components/RsvpForm.tsx
index 71db0d8..9ac2d53 100644
--- a/client/src/components/RsvpForm.tsx
+++ b/client/src/components/RsvpForm.tsx
@@ -16,20 +16,6 @@ import { useForm, Controller, useFieldArray } from 'react-hook-form';
import { useUpdateGuestMutation } from '../apiSlice';
import type { Guest } from '../apiSlice';
-type FormValues = {
- id: number;
- firstName: string;
- lastName: string;
- attendance: string;
- email: string;
- partySize: number;
- message: string;
- partyList: {
- firstName: string;
- lastName: string;
- }[];
-};
-
function RsvpForm() {
const [updateGuest] = useUpdateGuestMutation();
const guest: Guest = useOutletContext();
@@ -41,7 +27,7 @@ function RsvpForm() {
control,
watch,
formState: { errors },
- } = useForm<FormValues>({
+ } = useForm<Guest>({
defaultValues: {
id: guest?.id,
firstName: guest?.firstName,
@@ -163,6 +149,7 @@ function RsvpForm() {
helperText={errors.partySize?.message}
required
{...register('partySize', {
+ valueAsNumber: true,
onChange: handleParty,
required: 'This field is required',
min: { value: 1, message: 'Please enter a positive integer' },
diff --git a/client/src/features/auth/authSlice.ts b/client/src/features/auth/authSlice.ts
index bff2bdd..878de0c 100644
--- a/client/src/features/auth/authSlice.ts
+++ b/client/src/features/auth/authSlice.ts
@@ -1,4 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
+import type { PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../store';
import type { Guest } from '../../apiSlice';
@@ -11,8 +12,12 @@ const authSlice = createSlice({
name: 'auth',
initialState: { guest: undefined, token: undefined } as AuthState,
reducers: {
- setCredentials: (state, action) => {
- const { guest, token } = action.payload;
+ setCredentials: (
+ state,
+ {
+ payload: { guest, token },
+ }: PayloadAction<{ guest: Guest; token: string }>
+ ) => {
state.guest = guest;
state.token = token;
},
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 8ada188..88999f7 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -31,7 +31,7 @@ const router = createBrowserRouter([
element: <Registry />,
},
{
- path: 'guest-login',
+ path: 'guests/login',
element: <GuestLogin />,
},
{
@@ -51,19 +51,12 @@ const router = createBrowserRouter([
},
]);
-const enableMocking = async () => {
- const { worker } = await import('./mocks/browser');
- return worker.start();
-};
-
-enableMocking().then(() => {
- ReactDOM.createRoot(document.getElementById('root')!).render(
- <React.StrictMode>
- <Provider store={store}>
- <ThemeContextProvider>
- <RouterProvider router={router} />
- </ThemeContextProvider>
- </Provider>
- </React.StrictMode>
- );
-});
+ReactDOM.createRoot(document.getElementById('root')!).render(
+ <React.StrictMode>
+ <Provider store={store}>
+ <ThemeContextProvider>
+ <RouterProvider router={router} />
+ </ThemeContextProvider>
+ </Provider>
+ </React.StrictMode>
+);
diff --git a/client/src/mocks/handlers.ts b/client/src/mocks/handlers.ts
index 217a7d5..0e882ee 100644
--- a/client/src/mocks/handlers.ts
+++ b/client/src/mocks/handlers.ts
@@ -4,7 +4,7 @@ import { nanoid } from '@reduxjs/toolkit';
const token = nanoid();
export const handlers = [
- http.post('/guest-login', () => {
+ http.post('/guests/login', () => {
return HttpResponse.json({
guest: {
id: 1,
diff --git a/client/src/pages.ts b/client/src/pages.ts
index bad57f6..a1dbdfd 100644
--- a/client/src/pages.ts
+++ b/client/src/pages.ts
@@ -1,7 +1,7 @@
const pages = [
{ name: 'Home', to: '/' },
{ name: 'Schedule', to: '/schedule' },
- { name: 'RSVP', to: '/guest-login' },
+ { name: 'RSVP', to: '/guests/login' },
{ name: 'Registry', to: '/registry' },
];
diff --git a/server/cmd/main.go b/server/cmd/main.go
index f886e2b..5b81b66 100644
--- a/server/cmd/main.go
+++ b/server/cmd/main.go
@@ -2,6 +2,8 @@ package main
import (
"context"
+ "crypto/rand"
+ "encoding/base64"
"encoding/json"
"fmt"
"log"
@@ -9,6 +11,7 @@ import (
"os"
"regexp"
+ "github.com/golang-jwt/jwt/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.huntm.net/wedding/server/guests"
@@ -19,6 +22,7 @@ type guestHandler struct {
}
type guestStore interface {
+ FindGuest(creds guests.Credentials) (guests.Guest, error)
Get() ([]guests.Guest, error)
Add(guest guests.Guest) error
Update(guest guests.Guest) error
@@ -41,6 +45,60 @@ func newGuestHandler(s guestStore) *guestHandler {
}
}
+func (h *guestHandler) login(w http.ResponseWriter, r *http.Request) {
+ var creds guests.Credentials
+ err := json.NewDecoder(r.Body).Decode(&creds)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ guest, err := h.store.FindGuest(creds)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusUnauthorized)
+ return
+ }
+
+ claims := &guests.Claims{
+ Guest: guest,
+ RegisteredClaims: jwt.RegisteredClaims{},
+ }
+
+ key := make([]byte, 32)
+ _, err = rand.Read(key)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ secretKey := []byte(base64.StdEncoding.EncodeToString(key))
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenString, err := token.SignedString(secretKey)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ loginResponse := &guests.LoginResponse{
+ Guest: guest,
+ Token: tokenString,
+ }
+
+ jsonBytes, err := json.Marshal(loginResponse)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ http.SetCookie(w, &http.Cookie{
+ Name: "token",
+ Value: tokenString,
+ })
+ w.Write(jsonBytes)
+}
+
func (h *guestHandler) getGuests(w http.ResponseWriter, _ *http.Request) {
guests, err := h.store.Get()
if err != nil {
@@ -106,13 +164,17 @@ func (h *guestHandler) updateGuest(w http.ResponseWriter, r *http.Request) {
err = h.store.Update(guest)
if err != nil {
- http.Error(w, "Guest not found", http.StatusBadRequest)
+ http.Error(w, "Cannot update guest", http.StatusBadRequest)
return
}
}
func (h *guestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
+ case r.Method == http.MethodOptions:
+ w.WriteHeader(http.StatusOK)
+ case r.Method == http.MethodPost && r.URL.Path == "/guests/login":
+ h.login(w, r)
case r.Method == http.MethodGet && guestRe.MatchString(r.URL.Path):
h.getGuests(w, r)
case r.Method == http.MethodPost && guestRe.MatchString(r.URL.Path):
@@ -124,6 +186,23 @@ func (h *guestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+func enableCors(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if isPreflight(r) {
+ w.Header().Add("Access-Control-Allow-Methods", "*")
+ }
+ w.Header().Add("Access-Control-Allow-Origin", "*")
+ w.Header().Add("Access-Control-Allow-Headers", "*")
+ next.ServeHTTP(w, r)
+ })
+}
+
+func isPreflight(r *http.Request) bool {
+ return r.Method == "OPTIONS" &&
+ r.Header.Get("Origin") != "" &&
+ r.Header.Get("Access-Control-Request-Method") != ""
+}
+
func main() {
db, err := pgxpool.New(context.Background(), fmt.Sprintf("postgres://%s:%s@%s:%s/%s", user, pass, host, port, database))
if err != nil {
@@ -136,5 +215,5 @@ func main() {
mux := http.NewServeMux()
mux.Handle("/guests/", guestHandler)
- log.Fatal(http.ListenAndServe(":8080", mux))
+ log.Fatal(http.ListenAndServe(":8080", enableCors(mux)))
}
diff --git a/server/go.mod b/server/go.mod
index 3ab08ef..49af49e 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -2,13 +2,17 @@ module git.huntm.net/wedding/server
go 1.22.2
-require github.com/jackc/pgx/v5 v5.5.5
+require (
+ github.com/golang-jwt/jwt/v5 v5.2.1
+ github.com/jackc/pgx/v5 v5.5.5
+)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/crypto v0.23.0 // indirect
- golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.15.0 // indirect
)
diff --git a/server/go.sum b/server/go.sum
index 23b6653..b93b74b 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -1,10 +1,12 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
@@ -14,12 +16,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/server/guests/models.go b/server/guests/models.go
index c68a4c3..5915f81 100644
--- a/server/guests/models.go
+++ b/server/guests/models.go
@@ -1,5 +1,7 @@
package guests
+import "github.com/golang-jwt/jwt/v5"
+
type Guest struct {
Id int `json:"id"`
FirstName string `json:"firstName"`
@@ -15,3 +17,18 @@ type PartyGuest struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}
+
+type Credentials struct {
+ FirstName string `json:"firstName"`
+ LastName string `json:"lastName"`
+}
+
+type Claims struct {
+ Guest Guest `json:"guest"`
+ jwt.RegisteredClaims
+}
+
+type LoginResponse struct {
+ Guest Guest `json:"guest"`
+ Token string `json:"token"`
+}
diff --git a/server/guests/store.go b/server/guests/store.go
index 597cc80..bedc646 100644
--- a/server/guests/store.go
+++ b/server/guests/store.go
@@ -2,6 +2,7 @@ package guests
import (
"context"
+ "errors"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -16,6 +17,26 @@ func NewMemStore(db *pgxpool.Pool) *MemStore {
}
}
+func (m MemStore) FindGuest(creds Credentials) (Guest, error) {
+ rows, err := m.db.Query(context.Background(), "select * from guest")
+ var guest Guest
+ if err != nil {
+ return guest, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ err := rows.Scan(&guest.Id, &guest.FirstName, &guest.LastName, &guest.Attendance, &guest.Email, &guest.Message, &guest.PartySize)
+ if err != nil {
+ return guest, err
+ }
+ if guest.FirstName == creds.FirstName && guest.LastName == creds.LastName {
+ return guest, nil
+ }
+ }
+ return guest, errors.New("Guest does not exist")
+}
+
func (m MemStore) Get() ([]Guest, error) {
rows, err := m.db.Query(context.Background(), "select * from guest")
if err != nil {
@@ -79,9 +100,15 @@ func (m MemStore) Update(guest Guest) error {
return err
}
- statement = "update party set first_name = $1, last_name = $2 where guest_id = $3"
+ statement = "delete from party where guest_id = $1"
+ _, err = m.db.Exec(context.Background(), statement, guest.Id)
+ if err != nil {
+ return err
+ }
+
+ statement = "insert into party (guest_id, first_name, last_name) values ($1, $2, $3)"
for _, pg := range guest.PartyList {
- _, err = m.db.Exec(context.Background(), statement, pg.FirstName, pg.LastName, guest.Id)
+ _, err = m.db.Exec(context.Background(), statement, guest.Id, pg.FirstName, pg.LastName)
if err != nil {
return err
}