Fixed auth security
This commit is contained in:
@ -4,4 +4,5 @@ S3_ACCESS_KEY=your_access_key_here
|
|||||||
S3_SECRET_KEY=your_secret_key_here
|
S3_SECRET_KEY=your_secret_key_here
|
||||||
S3_BUCKET_NAME=your_bucket_name
|
S3_BUCKET_NAME=your_bucket_name
|
||||||
SECRET_CODE=super_secret_code
|
SECRET_CODE=super_secret_code
|
||||||
ADMIN_CODE=super_secret_code
|
ADMIN_CODE=super_secret_code
|
||||||
|
JWT_SECRET=your_jwt_secret_key_here_make_it_long_and_random
|
@ -13,11 +13,14 @@
|
|||||||
"@astrojs/react": "^4.2.0",
|
"@astrojs/react": "^4.2.0",
|
||||||
"@aws-sdk/client-s3": "^3.750.0",
|
"@aws-sdk/client-s3": "^3.750.0",
|
||||||
"@tailwindcss/vite": "^4.0.8",
|
"@tailwindcss/vite": "^4.0.8",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"astro": "^5.3.1",
|
"astro": "^5.3.1",
|
||||||
"astro-robots": "^2.3.1",
|
"astro-robots": "^2.3.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"tailwindcss": "^4.0.8",
|
"tailwindcss": "^4.0.8",
|
||||||
|
109
pnpm-lock.yaml
generated
109
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ importers:
|
|||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1))
|
version: 4.0.8(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1))
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.6
|
||||||
|
version: 9.0.9
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.13.5
|
specifier: ^22.13.5
|
||||||
version: 22.13.5
|
version: 22.13.5
|
||||||
@ -35,6 +38,12 @@ importers:
|
|||||||
astro-robots:
|
astro-robots:
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1(astro@5.3.1(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.34.8)(typescript@5.7.3))
|
version: 2.3.1(astro@5.3.1(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.34.8)(typescript@5.7.3))
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.2
|
||||||
|
version: 9.0.2
|
||||||
|
jwt-decode:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
react:
|
react:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0
|
version: 19.0.0
|
||||||
@ -1040,6 +1049,9 @@ packages:
|
|||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.9':
|
||||||
|
resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==}
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||||
|
|
||||||
@ -1145,6 +1157,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
camelcase@8.0.0:
|
camelcase@8.0.0:
|
||||||
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@ -1288,6 +1303,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@ -1522,6 +1540,20 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.2:
|
||||||
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jwa@1.4.1:
|
||||||
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
|
||||||
|
jws@3.2.2:
|
||||||
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
|
|
||||||
|
jwt-decode@4.0.0:
|
||||||
|
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
kleur@3.0.3:
|
kleur@3.0.3:
|
||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1602,6 +1634,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6:
|
||||||
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
|
||||||
|
lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
@ -1950,6 +2003,9 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
scheduler@0.25.0:
|
scheduler@0.25.0:
|
||||||
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
|
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
|
||||||
|
|
||||||
@ -3660,6 +3716,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
'@types/node': 22.13.5
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@ -3855,6 +3916,8 @@ snapshots:
|
|||||||
node-releases: 2.0.19
|
node-releases: 2.0.19
|
||||||
update-browserslist-db: 1.1.2(browserslist@4.24.4)
|
update-browserslist-db: 1.1.2(browserslist@4.24.4)
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
camelcase@8.0.0: {}
|
camelcase@8.0.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001700: {}
|
caniuse-lite@1.0.30001700: {}
|
||||||
@ -3958,6 +4021,10 @@ snapshots:
|
|||||||
|
|
||||||
dset@3.1.4: {}
|
dset@3.1.4: {}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.104: {}
|
electron-to-chromium@1.5.104: {}
|
||||||
@ -4223,6 +4290,32 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.2:
|
||||||
|
dependencies:
|
||||||
|
jws: 3.2.2
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.7.1
|
||||||
|
|
||||||
|
jwa@1.4.1:
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jws@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
jwa: 1.4.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jwt-decode@4.0.0: {}
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
kleur@4.1.5: {}
|
kleur@4.1.5: {}
|
||||||
@ -4283,6 +4376,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 4.1.0
|
p-locate: 4.1.0
|
||||||
|
|
||||||
|
lodash.includes@4.3.0: {}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4: {}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1: {}
|
||||||
|
|
||||||
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
@ -4874,6 +4981,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.34.8
|
'@rollup/rollup-win32-x64-msvc': 4.34.8
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
scheduler@0.25.0: {}
|
scheduler@0.25.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { fetchWithAuth } from "../utils/auth-client";
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
@ -37,7 +38,7 @@ const RSVPForm = () => {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||||
|
|
||||||
const response = await fetch("/api/rsvp", {
|
const response = await fetchWithAuth("/api/rsvp", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import type { RSVPItem } from "../lib/types";
|
import type { RSVPItem } from "../lib/types";
|
||||||
|
import { fetchWithAuth, getAuthToken } from "../utils/auth-client";
|
||||||
|
|
||||||
const RSVPManager = () => {
|
const RSVPManager = () => {
|
||||||
const [rsvpList, setRSVPList] = useState<RSVPItem[]>([]);
|
const [rsvpList, setRSVPList] = useState<RSVPItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchRSVPList();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchRSVPList = async () => {
|
const fetchRSVPList = async () => {
|
||||||
|
// Don't try to fetch if we don't have a token yet
|
||||||
|
if (!getAuthToken()) {
|
||||||
|
setError("No authentication token found");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/rsvp");
|
const response = await fetchWithAuth("/api/rsvp");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setRSVPList(data);
|
setRSVPList(data);
|
||||||
|
setError(null);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
console.error("Failed to fetch RSVP list:", e);
|
console.error("Failed to fetch RSVP list:", e);
|
||||||
@ -28,11 +33,25 @@ const RSVPManager = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRSVPList();
|
||||||
|
|
||||||
|
// Add listener for auth success to retry fetching
|
||||||
|
const handleAuthSuccess = () => {
|
||||||
|
fetchRSVPList();
|
||||||
|
};
|
||||||
|
document.addEventListener("auth-success", handleAuthSuccess);
|
||||||
|
return () => document.removeEventListener("auth-success", handleAuthSuccess);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="text-center">Loading RSVP list...</div>;
|
return <div className="text-center">Loading RSVP list...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
if (error === "No authentication token found") {
|
||||||
|
return <div className="text-center">Initializing...</div>;
|
||||||
|
}
|
||||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
return <div className="text-red-500 text-center">Error: {error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,31 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import type { RegistryItem } from "../lib/types";
|
import type { RegistryItem } from "../lib/types";
|
||||||
|
import { fetchWithAuth } from "../utils/auth-client";
|
||||||
|
|
||||||
const RegistryList = () => {
|
const RegistryList = () => {
|
||||||
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
||||||
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [claimantName, setClaimantName] = useState("");
|
const [claimantName, setClaimantName] = useState("");
|
||||||
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
|
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRegistryItems();
|
fetchRegistryItems();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Dispatch event if registry is empty
|
|
||||||
if (!loading && registryItems.length === 0) {
|
|
||||||
const event = new CustomEvent("registry-empty");
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}, [loading, registryItems]);
|
|
||||||
|
|
||||||
const fetchRegistryItems = async () => {
|
const fetchRegistryItems = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/registry");
|
const response = await fetchWithAuth("/api/registry");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setRegistryItems(data);
|
setRegistryItems(data);
|
||||||
|
if (data.length === 0) {
|
||||||
|
document.dispatchEvent(new Event("registry-empty"));
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
console.error("Failed to fetch registry items:", e);
|
console.error("Failed to fetch registry items:", e);
|
||||||
@ -39,7 +34,7 @@ const RegistryList = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckboxChange = (itemId: string) => {
|
const handleItemClick = (itemId: string) => {
|
||||||
const newClaimedItems = new Set(claimedItems);
|
const newClaimedItems = new Set(claimedItems);
|
||||||
if (newClaimedItems.has(itemId)) {
|
if (newClaimedItems.has(itemId)) {
|
||||||
newClaimedItems.delete(itemId);
|
newClaimedItems.delete(itemId);
|
||||||
@ -49,16 +44,6 @@ const RegistryList = () => {
|
|||||||
setClaimedItems(newClaimedItems);
|
setClaimedItems(newClaimedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleClaimantVisibility = (itemId: string) => {
|
|
||||||
const newShowClaimants = new Set(showClaimants);
|
|
||||||
if (newShowClaimants.has(itemId)) {
|
|
||||||
newShowClaimants.delete(itemId);
|
|
||||||
} else {
|
|
||||||
newShowClaimants.add(itemId);
|
|
||||||
}
|
|
||||||
setShowClaimants(newShowClaimants);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!claimantName.trim()) {
|
if (!claimantName.trim()) {
|
||||||
setError("Please enter your name before claiming items");
|
setError("Please enter your name before claiming items");
|
||||||
@ -79,7 +64,7 @@ const RegistryList = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for (const item of updates) {
|
for (const item of updates) {
|
||||||
const response = await fetch(`/api/registry/${item.id}`, {
|
const response = await fetchWithAuth(`/api/registry/${item.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -109,102 +94,84 @@ const RegistryList = () => {
|
|||||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
return <div className="text-red-500 text-center">Error: {error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const availableItems = registryItems.filter((item) => !item.taken);
|
||||||
|
const claimedItemsArray = registryItems.filter((item) => item.taken);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="text-2xl font-bold mb-4">Wedding Registry</h1>
|
{availableItems.length > 0 && (
|
||||||
<div className="overflow-x-auto">
|
<>
|
||||||
<table className="table w-full">
|
<div className="mb-4">
|
||||||
<thead>
|
<input
|
||||||
<tr>
|
type="text"
|
||||||
<th>Item</th>
|
value={claimantName}
|
||||||
<th>Link</th>
|
onChange={(e) => setClaimantName(e.target.value)}
|
||||||
<th>Status</th>
|
placeholder="Enter your name"
|
||||||
<th>Claim</th>
|
className="input input-bordered w-full max-w-xs"
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
|
||||||
{registryItems.map((item) => (
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||||
<tr key={item.id}>
|
{availableItems.map((item) => (
|
||||||
<td>
|
<div
|
||||||
<div className="flex flex-col gap-1">
|
key={item.id}
|
||||||
<div className="flex items-center gap-2">
|
className={`card bg-base-100 shadow-xl cursor-pointer ${
|
||||||
{item.name}
|
claimedItems.has(item.id)
|
||||||
{item.taken && (
|
? "border-4 border-primary"
|
||||||
<div className="flex items-center gap-2">
|
: ""
|
||||||
<span className="badge badge-success">Taken</span>
|
}`}
|
||||||
<button
|
onClick={() => handleItemClick(item.id)}
|
||||||
onClick={() => toggleClaimantVisibility(item.id)}
|
>
|
||||||
className="btn btn-xs btn-ghost"
|
<div className="card-body">
|
||||||
>
|
<h2 className="card-title">{item.name}</h2>
|
||||||
{showClaimants.has(item.id) ? "Hide" : "Show"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.taken &&
|
|
||||||
item.claimedBy &&
|
|
||||||
showClaimants.has(item.id) && (
|
|
||||||
<div className="text-sm text-gray-500 italic">
|
|
||||||
Claimed by: {item.claimedBy}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{item.link && (
|
{item.link && (
|
||||||
<a
|
<a
|
||||||
href={item.link}
|
href={item.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="link link-primary"
|
className="link link-primary"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
View
|
View Item
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</td>
|
</div>
|
||||||
<td>{item.taken ? "Claimed" : "Available"}</td>
|
</div>
|
||||||
<td>
|
))}
|
||||||
{!item.taken ? (
|
</div>
|
||||||
<input
|
|
||||||
type="checkbox"
|
{claimedItems.size > 0 && (
|
||||||
className="checkbox"
|
<div className="text-center mb-8">
|
||||||
checked={claimedItems.has(item.id)}
|
<button
|
||||||
onChange={() => handleCheckboxChange(item.id)}
|
onClick={handleSubmit}
|
||||||
/>
|
className="btn btn-primary"
|
||||||
) : <input
|
disabled={!claimantName.trim()}
|
||||||
type="checkbox"
|
>
|
||||||
className="checkbox"
|
Claim Selected Items
|
||||||
disabled
|
</button>
|
||||||
checked={true}
|
</div>
|
||||||
/>}
|
)}
|
||||||
</td>
|
</>
|
||||||
</tr>
|
)}
|
||||||
|
|
||||||
|
{claimedItemsArray.length > 0 && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Already Claimed</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{claimedItemsArray.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="card bg-base-100 shadow-xl opacity-50"
|
||||||
|
>
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">{item.name}</h2>
|
||||||
|
<p className="text-sm">
|
||||||
|
Claimed by: {item.claimedBy}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{claimedItems.size > 0 && (
|
|
||||||
<div className="mt-4 flex flex-col gap-4">
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text">Your Name</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="input input-bordered"
|
|
||||||
value={claimantName}
|
|
||||||
onChange={(e) => setClaimantName(e.target.value)}
|
|
||||||
placeholder="Enter your name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={!claimantName.trim()}
|
|
||||||
>
|
|
||||||
Submit Claims
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import type { RegistryItem } from "../lib/types";
|
import type { RegistryItem } from "../lib/types";
|
||||||
|
import { fetchWithAuth, getAuthToken } from "../utils/auth-client";
|
||||||
|
|
||||||
const RegistryManager = () => {
|
const RegistryManager = () => {
|
||||||
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
||||||
@ -8,20 +9,24 @@ const RegistryManager = () => {
|
|||||||
const [newItem, setNewItem] = useState({ name: "", link: "" });
|
const [newItem, setNewItem] = useState({ name: "", link: "" });
|
||||||
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
|
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchRegistryItems();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchRegistryItems = async () => {
|
const fetchRegistryItems = async () => {
|
||||||
|
// Don't try to fetch if we don't have a token yet
|
||||||
|
if (!getAuthToken()) {
|
||||||
|
setError("No authentication token found");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/registry");
|
const response = await fetchWithAuth("/api/registry");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setRegistryItems(data);
|
setRegistryItems(data);
|
||||||
|
setError(null);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
console.error("Failed to fetch registry items:", e);
|
console.error("Failed to fetch registry items:", e);
|
||||||
@ -30,12 +35,23 @@ const RegistryManager = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRegistryItems();
|
||||||
|
|
||||||
|
// Add listener for auth success to retry fetching
|
||||||
|
const handleAuthSuccess = () => {
|
||||||
|
fetchRegistryItems();
|
||||||
|
};
|
||||||
|
document.addEventListener("auth-success", handleAuthSuccess);
|
||||||
|
return () => document.removeEventListener("auth-success", handleAuthSuccess);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAddItem = async (e: React.FormEvent) => {
|
const handleAddItem = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newItem.name.trim()) return;
|
if (!newItem.name.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/registry", {
|
const response = await fetchWithAuth("/api/registry", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -55,11 +71,9 @@ const RegistryManager = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteItem = async (itemId: string) => {
|
const handleDeleteItem = async (id: string) => {
|
||||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/registry/${itemId}`, {
|
const response = await fetchWithAuth(`/api/registry/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,12 +88,12 @@ const RegistryManager = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleClaimantVisibility = (itemId: string) => {
|
const toggleShowClaimant = (id: string) => {
|
||||||
const newShowClaimants = new Set(showClaimants);
|
const newShowClaimants = new Set(showClaimants);
|
||||||
if (newShowClaimants.has(itemId)) {
|
if (newShowClaimants.has(id)) {
|
||||||
newShowClaimants.delete(itemId);
|
newShowClaimants.delete(id);
|
||||||
} else {
|
} else {
|
||||||
newShowClaimants.add(itemId);
|
newShowClaimants.add(id);
|
||||||
}
|
}
|
||||||
setShowClaimants(newShowClaimants);
|
setShowClaimants(newShowClaimants);
|
||||||
};
|
};
|
||||||
@ -89,50 +103,34 @@ const RegistryManager = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
if (error === "No authentication token found") {
|
||||||
|
return <div className="text-center">Initializing...</div>;
|
||||||
|
}
|
||||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
return <div className="text-red-500 text-center">Error: {error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<form onSubmit={handleAddItem} className="mb-8">
|
<form onSubmit={handleAddItem} className="mb-8">
|
||||||
<div className="card bg-base-100 shadow-xl">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="card-body">
|
<input
|
||||||
<h2 className="card-title">Add New Item</h2>
|
type="text"
|
||||||
<div className="form-control">
|
value={newItem.name}
|
||||||
<label className="label">
|
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||||
<span className="label-text">Item Name</span>
|
placeholder="Item name"
|
||||||
</label>
|
className="input input-bordered"
|
||||||
<input
|
required
|
||||||
type="text"
|
/>
|
||||||
placeholder="Enter item name"
|
<input
|
||||||
className="input input-bordered"
|
type="url"
|
||||||
value={newItem.name}
|
value={newItem.link}
|
||||||
onChange={(e) =>
|
onChange={(e) => setNewItem({ ...newItem, link: e.target.value })}
|
||||||
setNewItem({ ...newItem, name: e.target.value })
|
placeholder="Item link (optional)"
|
||||||
}
|
className="input input-bordered"
|
||||||
required
|
/>
|
||||||
/>
|
<button type="submit" className="btn btn-primary">
|
||||||
</div>
|
Add Item
|
||||||
<div className="form-control">
|
</button>
|
||||||
<label className="label">
|
|
||||||
<span className="label-text">Item Link (optional)</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
placeholder="Enter item link"
|
|
||||||
className="input input-bordered"
|
|
||||||
value={newItem.link}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, link: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="card-actions justify-end">
|
|
||||||
<button type="submit" className="btn btn-primary">
|
|
||||||
Add Item
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -140,7 +138,7 @@ const RegistryManager = () => {
|
|||||||
<table className="table w-full">
|
<table className="table w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Item</th>
|
<th>Name</th>
|
||||||
<th>Link</th>
|
<th>Link</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
@ -149,35 +147,7 @@ const RegistryManager = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{registryItems.map((item) => (
|
{registryItems.map((item) => (
|
||||||
<tr key={item.id}>
|
<tr key={item.id}>
|
||||||
<td>
|
<td>{item.name}</td>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{item.name}
|
|
||||||
{item.taken && (
|
|
||||||
<>
|
|
||||||
<span className="badge badge-success">Taken</span>
|
|
||||||
{item.claimedBy && (
|
|
||||||
<button
|
|
||||||
onClick={() => toggleClaimantVisibility(item.id)}
|
|
||||||
className="btn btn-ghost btn-xs"
|
|
||||||
>
|
|
||||||
{showClaimants.has(item.id) ? (
|
|
||||||
<i className="fas fa-eye-slash" />
|
|
||||||
) : (
|
|
||||||
<i className="fas fa-eye" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.taken &&
|
|
||||||
item.claimedBy &&
|
|
||||||
showClaimants.has(item.id) && (
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
Claimed by: {item.claimedBy}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
{item.link && (
|
{item.link && (
|
||||||
<a
|
<a
|
||||||
@ -186,15 +156,38 @@ const RegistryManager = () => {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="link link-primary"
|
className="link link-primary"
|
||||||
>
|
>
|
||||||
View
|
View Item
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{item.taken ? "Claimed" : "Available"}</td>
|
<td>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`badge ${
|
||||||
|
item.taken ? "badge-error" : "badge-success"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.taken ? "Claimed" : "Available"}
|
||||||
|
</span>
|
||||||
|
{item.taken && (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleShowClaimant(item.id)}
|
||||||
|
className="btn btn-xs"
|
||||||
|
>
|
||||||
|
{showClaimants.has(item.id) ? "Hide" : "Show"} Claimant
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{item.taken && showClaimants.has(item.id) && (
|
||||||
|
<div className="text-sm mt-1">
|
||||||
|
Claimed by: {item.claimedBy}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className="btn btn-error btn-sm"
|
|
||||||
onClick={() => handleDeleteItem(item.id)}
|
onClick={() => handleDeleteItem(item.id)}
|
||||||
|
className="btn btn-error btn-sm"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { setAuthData } from "../utils/auth-client";
|
||||||
|
|
||||||
interface SignInProps {
|
interface SignInProps {
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
@ -8,7 +9,7 @@ interface SignInProps {
|
|||||||
interface ApiResponse {
|
interface ApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
role?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
||||||
@ -16,16 +17,17 @@ const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Initialize disabled state to true since code is empty initially
|
||||||
|
const isDisabled = !code || isSubmitting;
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
if (isDisabled) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear existing authentication before attempting new sign-in
|
|
||||||
sessionStorage.removeItem("isAuthenticated");
|
|
||||||
sessionStorage.removeItem("role");
|
|
||||||
|
|
||||||
const response = await fetch("/api/auth", {
|
const response = await fetch("/api/auth", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -36,21 +38,11 @@ const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
|||||||
|
|
||||||
const data: ApiResponse = await response.json();
|
const data: ApiResponse = await response.json();
|
||||||
|
|
||||||
if (!response.ok || !data.success) {
|
if (!response.ok || !data.success || !data.token) {
|
||||||
throw new Error(data.error || "Invalid code");
|
throw new Error(data.error || "Invalid code");
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem("isAuthenticated", "true");
|
setAuthData(data.token);
|
||||||
sessionStorage.setItem("role", data.role || "guest");
|
|
||||||
|
|
||||||
// Dispatch a custom event with the new role
|
|
||||||
const event = new CustomEvent("auth-success", {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
detail: { role: data.role }
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err instanceof Error ? err.message : "Authentication failed");
|
setError(err instanceof Error ? err.message : "Authentication failed");
|
||||||
@ -89,7 +81,7 @@ const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
disabled={!code || isSubmitting}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Verifying..." : "Continue"}
|
{isSubmitting ? "Verifying..." : "Continue"}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<button class="btn btn-secondary" onclick="handleSignOut()">Sign Out</button>
|
<button id="signout-btn" class="btn btn-secondary">Sign Out</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function handleSignOut() {
|
import { clearAuthData } from "../utils/auth-client";
|
||||||
sessionStorage.removeItem("isAuthenticated");
|
|
||||||
sessionStorage.removeItem("role");
|
// Add click handler to the button
|
||||||
window.location.reload();
|
document.getElementById("signout-btn")?.addEventListener("click", () => {
|
||||||
}
|
clearAuthData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
@ -1,30 +1,24 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { clearAuthData, isAuthenticated } from "../utils/auth-client";
|
||||||
|
|
||||||
const SignOut = () => {
|
const SignOut = () => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isAuthenticated = sessionStorage.getItem("isAuthenticated") === "true";
|
setIsVisible(isAuthenticated());
|
||||||
setIsVisible(isAuthenticated);
|
|
||||||
|
|
||||||
const handleAuthChange = () => {
|
const handleAuthChange = () => {
|
||||||
setIsVisible(sessionStorage.getItem("isAuthenticated") === "true");
|
setIsVisible(isAuthenticated());
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("auth-success", handleAuthChange);
|
document.addEventListener("auth-success", handleAuthChange);
|
||||||
return () => document.removeEventListener("auth-success", handleAuthChange);
|
return () => document.removeEventListener("auth-success", handleAuthChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSignOut = () => {
|
|
||||||
sessionStorage.removeItem("isAuthenticated");
|
|
||||||
sessionStorage.removeItem("role");
|
|
||||||
window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
if (!isVisible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={handleSignOut} className="btn btn-secondary">
|
<button onClick={clearAuthData} className="btn btn-secondary">
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ interface Props {
|
|||||||
const { title } = Astro.props;
|
const { title } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="auth-container" class="flex flex-col gap-4">
|
<div id="auth-container">
|
||||||
<SignIn client:load onSuccess={() => {}} requiredRole="guest" />
|
<SignIn client:load onSuccess={() => {}} requiredRole="guest" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -17,16 +17,24 @@ const { title } = Astro.props;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Check auth state on page load
|
import { isAuthenticated } from "../utils/auth-client";
|
||||||
const isAuthenticated = sessionStorage.getItem("isAuthenticated") === "true";
|
|
||||||
if (isAuthenticated) {
|
function updateVisibility() {
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
const authContainer = document.getElementById("auth-container");
|
||||||
document.getElementById("content-container")?.classList.remove("hidden");
|
const contentContainer = document.getElementById("content-container");
|
||||||
|
|
||||||
|
if (isAuthenticated()) {
|
||||||
|
authContainer?.classList.add("hidden");
|
||||||
|
contentContainer?.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
authContainer?.classList.remove("hidden");
|
||||||
|
contentContainer?.classList.add("hidden");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check auth state on page load
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
// Add event listener for custom event from SignIn component
|
// Add event listener for custom event from SignIn component
|
||||||
document.addEventListener("auth-success", () => {
|
document.addEventListener("auth-success", updateVisibility);
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
|
||||||
document.getElementById("content-container")?.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
import AuthLayout from "./AuthLayout.astro";
|
|
||||||
import Navigation from "../components/Navigation.astro";
|
import Navigation from "../components/Navigation.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -26,9 +25,7 @@ const { title } = Astro.props;
|
|||||||
<body>
|
<body>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div class="flex items-center justify-center min-h-screen">
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
<AuthLayout title={title}>
|
<slot />
|
||||||
<slot />
|
|
||||||
</AuthLayout>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
|
import { generateToken } from "../../utils/jwt";
|
||||||
|
|
||||||
interface AuthRequest {
|
interface AuthRequest {
|
||||||
code: string;
|
code: string;
|
||||||
@ -15,11 +16,15 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const secretCode = process.env.SECRET_CODE || import.meta.env.SECRET_CODE;
|
const secretCode = process.env.SECRET_CODE || import.meta.env.SECRET_CODE;
|
||||||
const adminCode = process.env.ADMIN_CODE || import.meta.env.ADMIN_CODE;
|
const adminCode = process.env.ADMIN_CODE || import.meta.env.ADMIN_CODE;
|
||||||
|
|
||||||
let role = "guest";
|
let role: "guest" | "admin" | null = null;
|
||||||
|
|
||||||
if (code === adminCode) {
|
if (code === adminCode) {
|
||||||
role = "admin";
|
role = "admin";
|
||||||
} else if (code !== secretCode) {
|
} else if (code === secretCode) {
|
||||||
|
role = "guest";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
@ -45,10 +50,19 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify({ success: true, role }), {
|
const token = generateToken(role);
|
||||||
status: 200,
|
|
||||||
headers,
|
return new Response(
|
||||||
});
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
role
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Authentication failed:", error);
|
console.error("Authentication failed:", error);
|
||||||
return new Response(
|
return new Response(
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { getS3Data, putS3Data } from "../../../lib/s3";
|
import { getS3Data, putS3Data } from "../../../lib/s3";
|
||||||
import type { RegistryItem } from "../../../lib/types";
|
import type { RegistryItem } from "../../../lib/types";
|
||||||
|
import { createProtectedAPIRoute } from "../../../utils/auth-middleware";
|
||||||
|
|
||||||
const REGISTRY_FILE_KEY = "registry.json";
|
const REGISTRY_FILE_KEY = "registry.json";
|
||||||
|
|
||||||
// GET: Get a specific registry item by ID
|
// GET: Get a specific registry item by ID (requires guest role)
|
||||||
export const GET: APIRoute = async ({ params }) => {
|
const handleGet: APIRoute = async ({ params }) => {
|
||||||
try {
|
try {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -38,8 +39,8 @@ export const GET: APIRoute = async ({ params }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// PUT: Update an existing registry item
|
// PUT: Update an existing registry item (requires guest role)
|
||||||
export const PUT: APIRoute = async ({ request, params }) => {
|
const handlePut: APIRoute = async ({ request, params }) => {
|
||||||
try {
|
try {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -83,9 +84,8 @@ export const PUT: APIRoute = async ({ request, params }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// DELETE: Delete a registry item (requires admin role)
|
||||||
// DELETE: Delete a registry item
|
const handleDelete: APIRoute = async ({ params }) => {
|
||||||
export const DELETE: APIRoute = async ({ params }) => {
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@ -134,3 +134,8 @@ export const DELETE: APIRoute = async ({ params }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export protected routes
|
||||||
|
export const GET = createProtectedAPIRoute(handleGet, "guest");
|
||||||
|
export const PUT = createProtectedAPIRoute(handlePut, "guest");
|
||||||
|
export const DELETE = createProtectedAPIRoute(handleDelete, "admin");
|
||||||
|
@ -2,11 +2,12 @@ import type { APIRoute } from "astro";
|
|||||||
import { getS3Data, putS3Data } from "../../../lib/s3";
|
import { getS3Data, putS3Data } from "../../../lib/s3";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import type { RegistryItem } from "../../../lib/types";
|
import type { RegistryItem } from "../../../lib/types";
|
||||||
|
import { createProtectedAPIRoute } from "../../../utils/auth-middleware";
|
||||||
|
|
||||||
const REGISTRY_FILE_KEY = "registry.json";
|
const REGISTRY_FILE_KEY = "registry.json";
|
||||||
|
|
||||||
// GET: List all registry items
|
// GET: List all registry items (requires guest role)
|
||||||
export const GET: APIRoute = async () => {
|
const handleGet: APIRoute = async () => {
|
||||||
try {
|
try {
|
||||||
const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || [];
|
const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || [];
|
||||||
return new Response(JSON.stringify(registry), {
|
return new Response(JSON.stringify(registry), {
|
||||||
@ -22,8 +23,8 @@ export const GET: APIRoute = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// POST: Create a new registry item
|
// POST: Create a new registry item (requires admin role)
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
const handlePost: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { name, link } = body;
|
const { name, link } = body;
|
||||||
@ -58,3 +59,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export protected routes
|
||||||
|
export const GET = createProtectedAPIRoute(handleGet, "guest");
|
||||||
|
export const POST = createProtectedAPIRoute(handlePost, "admin");
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { getS3Data, putS3Data } from "../../lib/s3";
|
import { getS3Data, putS3Data } from "../../lib/s3";
|
||||||
import type { RSVPItem } from "../../lib/types";
|
import type { RSVPItem } from "../../lib/types";
|
||||||
|
import { createProtectedAPIRoute } from "../../utils/auth-middleware";
|
||||||
|
|
||||||
const objectsToCSV = (data: RSVPItem[]): string => {
|
const objectsToCSV = (data: RSVPItem[]): string => {
|
||||||
const headers = ["name", "dietaryRestrictions", "notes", "timestamp"];
|
const headers = ["name", "dietaryRestrictions", "notes", "timestamp"];
|
||||||
@ -44,8 +45,8 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// GET: Retrieve all RSVPs
|
// GET: Retrieve all RSVPs (requires admin role)
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
const handleGet: APIRoute = async ({ request }) => {
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@ -81,8 +82,8 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// POST: Submit a new RSVP
|
// POST: Submit a new RSVP (requires guest role)
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
const handlePost: APIRoute = async ({ request }) => {
|
||||||
console.log("API endpoint hit - starting request processing");
|
console.log("API endpoint hit - starting request processing");
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
@ -136,3 +137,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export protected routes
|
||||||
|
export const GET = createProtectedAPIRoute(handleGet, "admin");
|
||||||
|
export const POST = createProtectedAPIRoute(handlePost, "guest");
|
||||||
|
@ -15,40 +15,33 @@ import SignOut from "../../components/SignOut.tsx";
|
|||||||
|
|
||||||
<div id="manager-container" class="hidden">
|
<div id="manager-container" class="hidden">
|
||||||
<RegistryManager client:load />
|
<RegistryManager client:load />
|
||||||
</div>
|
<div class="flex flex-row gap-2 justify-center items-center mt-4">
|
||||||
|
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||||
<div class="flex flex-row gap-2 justify-center items-center">
|
<SignOut client:load />
|
||||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const checkAndUpdateVisibility = (role: string | null) => {
|
import { hasRole } from "../../utils/auth-client";
|
||||||
if (role === "admin") {
|
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
function updateVisibility() {
|
||||||
document
|
const authContainer = document.getElementById("auth-container");
|
||||||
.getElementById("manager-container")
|
const managerContainer = document.getElementById("manager-container");
|
||||||
?.classList.remove("hidden");
|
|
||||||
|
if (hasRole("admin")) {
|
||||||
|
authContainer?.classList.add("hidden");
|
||||||
|
managerContainer?.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
document
|
authContainer?.classList.remove("hidden");
|
||||||
.getElementById("auth-container")
|
managerContainer?.classList.add("hidden");
|
||||||
?.classList.remove("hidden");
|
|
||||||
document
|
|
||||||
.getElementById("manager-container")
|
|
||||||
?.classList.add("hidden");
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Check auth state on page load
|
// Check auth state on page load
|
||||||
const isAuthenticated =
|
updateVisibility();
|
||||||
sessionStorage.getItem("isAuthenticated") === "true";
|
|
||||||
const role = sessionStorage.getItem("role");
|
|
||||||
checkAndUpdateVisibility(role);
|
|
||||||
|
|
||||||
// Add event listener for custom event from SignIn component
|
// Add event listener for custom event from SignIn component
|
||||||
document.addEventListener("auth-success", ((event: CustomEvent) => {
|
document.addEventListener("auth-success", updateVisibility);
|
||||||
const newRole = event.detail?.role || sessionStorage.getItem("role");
|
|
||||||
checkAndUpdateVisibility(newRole);
|
|
||||||
}) as EventListener);
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import RegistryList from "../../components/RegistryList.tsx";
|
import RegistryList from "../../components/RegistryList.tsx";
|
||||||
|
import SignIn from "../../components/SignIn.tsx";
|
||||||
import SignOut from "../../components/SignOut.tsx";
|
import SignOut from "../../components/SignOut.tsx";
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -10,37 +11,44 @@ import SignOut from "../../components/SignOut.tsx";
|
|||||||
View and Claim Items from the Registry:
|
View and Claim Items from the Registry:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="registry-container">
|
<div id="auth-container">
|
||||||
|
<SignIn client:load onSuccess={() => {}} requiredRole="guest" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content-container" class="hidden">
|
||||||
<RegistryList client:load />
|
<RegistryList client:load />
|
||||||
<div id="empty-registry-message" class="text-center p-8 hidden">
|
<div id="empty-registry-message" class="text-center p-8 hidden">
|
||||||
<p class="text-xl">Nothing here yet! Please check back in a week!</p>
|
<p class="text-xl">Nothing here yet! Please check back in a week!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex flex-row gap-2 justify-center items-center mt-4">
|
||||||
|
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||||
<div class="flex flex-row gap-2 justify-center items-center">
|
<SignOut client:load />
|
||||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Check auth state on page load
|
import { hasRole, isAuthenticated } from "../../utils/auth-client";
|
||||||
const isAuthenticated =
|
|
||||||
sessionStorage.getItem("isAuthenticated") === "true";
|
function updateVisibility() {
|
||||||
if (isAuthenticated) {
|
const authContainer = document.getElementById("auth-container");
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
const contentContainer = document.getElementById("content-container");
|
||||||
document
|
|
||||||
.getElementById("registry-container")
|
if (isAuthenticated() && hasRole("guest")) {
|
||||||
?.classList.remove("hidden");
|
authContainer?.classList.add("hidden");
|
||||||
|
contentContainer?.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
authContainer?.classList.remove("hidden");
|
||||||
|
contentContainer?.classList.add("hidden");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check auth state on page load
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
// Add event listener for custom event from SignIn component
|
// Add event listener for custom event from SignIn component
|
||||||
document.addEventListener("auth-success", () => {
|
document.addEventListener("auth-success", updateVisibility);
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
|
||||||
document
|
|
||||||
.getElementById("registry-container")
|
|
||||||
?.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for empty registry
|
// Check for empty registry
|
||||||
document.addEventListener("registry-empty", () => {
|
document.addEventListener("registry-empty", () => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from "../layouts/Layout.astro";
|
||||||
import RSVP from "../components/RSVP.tsx";
|
import RSVP from "../components/RSVP.tsx";
|
||||||
|
import SignIn from "../components/SignIn.tsx";
|
||||||
import SignOut from "../components/SignOut.tsx";
|
import SignOut from "../components/SignOut.tsx";
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -10,26 +11,39 @@ import SignOut from "../components/SignOut.tsx";
|
|||||||
Please RSVP using the form below:
|
Please RSVP using the form below:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RSVP client:load />
|
<div id="auth-container">
|
||||||
|
<SignIn client:load onSuccess={() => {}} requiredRole="guest" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-2 justify-center items-center">
|
<div id="content-container" class="hidden">
|
||||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
<RSVP client:load />
|
||||||
|
<div class="flex flex-row gap-2 justify-center items-center mt-4">
|
||||||
|
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||||
|
<SignOut client:load />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Check auth state on page load
|
import { hasRole, isAuthenticated } from "../utils/auth-client";
|
||||||
const isAuthenticated =
|
|
||||||
sessionStorage.getItem("isAuthenticated") === "true";
|
function updateVisibility() {
|
||||||
if (isAuthenticated) {
|
const authContainer = document.getElementById("auth-container");
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
const contentContainer = document.getElementById("content-container");
|
||||||
document.getElementById("rsvp-container")?.classList.remove("hidden");
|
|
||||||
|
if (isAuthenticated() && hasRole("guest")) {
|
||||||
|
authContainer?.classList.add("hidden");
|
||||||
|
contentContainer?.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
authContainer?.classList.remove("hidden");
|
||||||
|
contentContainer?.classList.add("hidden");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check auth state on page load
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
// Add event listener for custom event from SignIn component
|
// Add event listener for custom event from SignIn component
|
||||||
document.addEventListener("auth-success", () => {
|
document.addEventListener("auth-success", updateVisibility);
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
|
||||||
document.getElementById("rsvp-container")?.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -15,40 +15,33 @@ import SignOut from "../../components/SignOut.tsx";
|
|||||||
|
|
||||||
<div id="manager-container" class="hidden">
|
<div id="manager-container" class="hidden">
|
||||||
<RSVPManager client:load />
|
<RSVPManager client:load />
|
||||||
</div>
|
<div class="flex flex-row gap-2 justify-center items-center mt-4">
|
||||||
|
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||||
<div class="flex flex-row gap-2 justify-center items-center">
|
<SignOut client:load />
|
||||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const checkAndUpdateVisibility = (role: string | null) => {
|
import { hasRole } from "../../utils/auth-client";
|
||||||
if (role === "admin") {
|
|
||||||
document.getElementById("auth-container")?.classList.add("hidden");
|
function updateVisibility() {
|
||||||
document
|
const authContainer = document.getElementById("auth-container");
|
||||||
.getElementById("manager-container")
|
const managerContainer = document.getElementById("manager-container");
|
||||||
?.classList.remove("hidden");
|
|
||||||
|
if (hasRole("admin")) {
|
||||||
|
authContainer?.classList.add("hidden");
|
||||||
|
managerContainer?.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
document
|
authContainer?.classList.remove("hidden");
|
||||||
.getElementById("auth-container")
|
managerContainer?.classList.add("hidden");
|
||||||
?.classList.remove("hidden");
|
|
||||||
document
|
|
||||||
.getElementById("manager-container")
|
|
||||||
?.classList.add("hidden");
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Check auth state on page load
|
// Check auth state on page load
|
||||||
const isAuthenticated =
|
updateVisibility();
|
||||||
sessionStorage.getItem("isAuthenticated") === "true";
|
|
||||||
const role = sessionStorage.getItem("role");
|
|
||||||
checkAndUpdateVisibility(role);
|
|
||||||
|
|
||||||
// Add event listener for custom event from SignIn component
|
// Add event listener for custom event from SignIn component
|
||||||
document.addEventListener("auth-success", ((event: CustomEvent) => {
|
document.addEventListener("auth-success", updateVisibility);
|
||||||
const newRole = event.detail?.role || sessionStorage.getItem("role");
|
|
||||||
checkAndUpdateVisibility(newRole);
|
|
||||||
}) as EventListener);
|
|
||||||
</script>
|
</script>
|
49
src/pages/rsvp/index.astro
Normal file
49
src/pages/rsvp/index.astro
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import RSVP from "../../components/RSVP.tsx";
|
||||||
|
import SignIn from "../../components/SignIn.tsx";
|
||||||
|
import SignOut from "../../components/SignOut.tsx";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="RSVP">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="text-center text-4xl">
|
||||||
|
Please RSVP using the form below:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="auth-container">
|
||||||
|
<SignIn client:load onSuccess={() => {}} requiredRole="guest" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content-container" class="hidden">
|
||||||
|
<RSVP client:load />
|
||||||
|
<div class="flex flex-row gap-2 justify-center items-center mt-4">
|
||||||
|
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||||
|
<SignOut client:load />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { hasRole } from "../../utils/auth-client";
|
||||||
|
|
||||||
|
function updateVisibility() {
|
||||||
|
const authContainer = document.getElementById("auth-container");
|
||||||
|
const contentContainer = document.getElementById("content-container");
|
||||||
|
|
||||||
|
if (hasRole("guest")) {
|
||||||
|
authContainer?.classList.add("hidden");
|
||||||
|
contentContainer?.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
authContainer?.classList.remove("hidden");
|
||||||
|
contentContainer?.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth state on page load
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
|
// Add event listener for custom event from SignIn component
|
||||||
|
document.addEventListener("auth-success", updateVisibility);
|
||||||
|
</script>
|
77
src/utils/auth-client.ts
Normal file
77
src/utils/auth-client.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
|
interface JWTPayload {
|
||||||
|
role: string;
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAuthData(token: string) {
|
||||||
|
sessionStorage.setItem('authToken', token);
|
||||||
|
|
||||||
|
// Dispatch auth event
|
||||||
|
const payload = jwtDecode<JWTPayload>(token);
|
||||||
|
const event = new CustomEvent("auth-success", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { role: payload?.role }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAuthData() {
|
||||||
|
sessionStorage.removeItem('authToken');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthToken(): string | null {
|
||||||
|
return sessionStorage.getItem('authToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRole(): string | null {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = jwtDecode<JWTPayload>(token);
|
||||||
|
// Check if token is expired
|
||||||
|
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
||||||
|
clearAuthData(); // Clear expired token
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return payload.role;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding token:', error);
|
||||||
|
clearAuthData(); // Clear invalid token
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAuthenticated(): boolean {
|
||||||
|
return getRole() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasRole(requiredRole: "guest" | "admin"): boolean {
|
||||||
|
const role = getRole();
|
||||||
|
if (!role) return false;
|
||||||
|
if (requiredRole === "admin") return role === "admin";
|
||||||
|
if (requiredRole === "guest") return role === "guest" || role === "admin";
|
||||||
|
return false;
|
||||||
|
} // admin can access guest pages, but guest can't access admin pages
|
||||||
|
|
||||||
|
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No authentication token found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...options.headers,
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
50
src/utils/auth-middleware.ts
Normal file
50
src/utils/auth-middleware.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { APIContext, APIRoute } from "astro";
|
||||||
|
import { verifyToken, extractTokenFromHeader } from "./jwt";
|
||||||
|
|
||||||
|
export async function validateAuth(
|
||||||
|
context: APIContext,
|
||||||
|
requiredRole?: "guest" | "admin"
|
||||||
|
): Promise<{ isValid: boolean; role?: string; error?: string }> {
|
||||||
|
const authHeader = context.request.headers.get("Authorization");
|
||||||
|
const token = extractTokenFromHeader(authHeader || "");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return { isValid: false, error: "No token provided" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = verifyToken(token);
|
||||||
|
if (!payload) {
|
||||||
|
return { isValid: false, error: "Invalid token" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredRole && payload.role !== requiredRole) {
|
||||||
|
if (requiredRole === "admin" && payload.role !== "admin") {
|
||||||
|
return { isValid: false, error: "Admin access required" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true, role: payload.role };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProtectedAPIRoute(handler: Function, requiredRole?: "guest" | "admin"): APIRoute {
|
||||||
|
return async (context: APIContext) => {
|
||||||
|
const { isValid, error } = await validateAuth(context, requiredRole);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: error || "Authentication failed",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(context);
|
||||||
|
};
|
||||||
|
}
|
33
src/utils/jwt.ts
Normal file
33
src/utils/jwt.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
interface JWTPayload {
|
||||||
|
role: string;
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || import.meta.env.JWT_SECRET;
|
||||||
|
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
throw new Error('JWT_SECRET environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateToken(role: string): string {
|
||||||
|
return jwt.sign({ role }, JWT_SECRET, { expiresIn: '24h' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyToken(token: string): JWTPayload | null {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, JWT_SECRET) as JWTPayload;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JWT verification failed:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTokenFromHeader(authHeader?: string): string | null {
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authHeader.split(' ')[1];
|
||||||
|
}
|
Reference in New Issue
Block a user