Fixed auth security
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { fetchWithAuth } from "../utils/auth-client";
|
||||
import "../styles/global.css";
|
||||
|
||||
interface FormData {
|
||||
@ -37,7 +38,7 @@ const RSVPForm = () => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||
|
||||
const response = await fetch("/api/rsvp", {
|
||||
const response = await fetchWithAuth("/api/rsvp", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -1,25 +1,30 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { RSVPItem } from "../lib/types";
|
||||
import { fetchWithAuth, getAuthToken } from "../utils/auth-client";
|
||||
|
||||
const RSVPManager = () => {
|
||||
const [rsvpList, setRSVPList] = useState<RSVPItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRSVPList();
|
||||
}, []);
|
||||
|
||||
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);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch("/api/rsvp");
|
||||
const response = await fetchWithAuth("/api/rsvp");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setRSVPList(data);
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
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) {
|
||||
return <div className="text-center">Loading RSVP list...</div>;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
|
@ -1,36 +1,31 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { RegistryItem } from "../lib/types";
|
||||
import { fetchWithAuth } from "../utils/auth-client";
|
||||
|
||||
const RegistryList = () => {
|
||||
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
||||
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [claimantName, setClaimantName] = useState("");
|
||||
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
|
||||
const [claimedItems, setClaimedItems] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch("/api/registry");
|
||||
const response = await fetchWithAuth("/api/registry");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setRegistryItems(data);
|
||||
if (data.length === 0) {
|
||||
document.dispatchEvent(new Event("registry-empty"));
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
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);
|
||||
if (newClaimedItems.has(itemId)) {
|
||||
newClaimedItems.delete(itemId);
|
||||
@ -49,16 +44,6 @@ const RegistryList = () => {
|
||||
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 () => {
|
||||
if (!claimantName.trim()) {
|
||||
setError("Please enter your name before claiming items");
|
||||
@ -79,7 +64,7 @@ const RegistryList = () => {
|
||||
|
||||
try {
|
||||
for (const item of updates) {
|
||||
const response = await fetch(`/api/registry/${item.id}`, {
|
||||
const response = await fetchWithAuth(`/api/registry/${item.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -109,102 +94,84 @@ const RegistryList = () => {
|
||||
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 (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Wedding Registry</h1>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Link</th>
|
||||
<th>Status</th>
|
||||
<th>Claim</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{registryItems.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.name}
|
||||
{item.taken && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="badge badge-success">Taken</span>
|
||||
<button
|
||||
onClick={() => toggleClaimantVisibility(item.id)}
|
||||
className="btn btn-xs btn-ghost"
|
||||
>
|
||||
{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>
|
||||
{availableItems.length > 0 && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={claimantName}
|
||||
onChange={(e) => setClaimantName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
className="input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
{availableItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`card bg-base-100 shadow-xl cursor-pointer ${
|
||||
claimedItems.has(item.id)
|
||||
? "border-4 border-primary"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleItemClick(item.id)}
|
||||
>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">{item.name}</h2>
|
||||
{item.link && (
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link link-primary"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View
|
||||
View Item
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td>{item.taken ? "Claimed" : "Available"}</td>
|
||||
<td>
|
||||
{!item.taken ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={claimedItems.has(item.id)}
|
||||
onChange={() => handleCheckboxChange(item.id)}
|
||||
/>
|
||||
) : <input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
disabled
|
||||
checked={true}
|
||||
/>}
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{claimedItems.size > 0 && (
|
||||
<div className="text-center mb-8">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="btn btn-primary"
|
||||
disabled={!claimantName.trim()}
|
||||
>
|
||||
Claim Selected Items
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!claimantName.trim()}
|
||||
>
|
||||
Submit Claims
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { RegistryItem } from "../lib/types";
|
||||
import { fetchWithAuth, getAuthToken } from "../utils/auth-client";
|
||||
|
||||
const RegistryManager = () => {
|
||||
const [registryItems, setRegistryItems] = useState<RegistryItem[]>([]);
|
||||
@ -8,20 +9,24 @@ const RegistryManager = () => {
|
||||
const [newItem, setNewItem] = useState({ name: "", link: "" });
|
||||
const [showClaimants, setShowClaimants] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
fetchRegistryItems();
|
||||
}, []);
|
||||
|
||||
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);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch("/api/registry");
|
||||
const response = await fetchWithAuth("/api/registry");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setRegistryItems(data);
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
if (!newItem.name.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/registry", {
|
||||
const response = await fetchWithAuth("/api/registry", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -55,11 +71,9 @@ const RegistryManager = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (itemId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||
|
||||
const handleDeleteItem = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/registry/${itemId}`, {
|
||||
const response = await fetchWithAuth(`/api/registry/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
@ -74,12 +88,12 @@ const RegistryManager = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleClaimantVisibility = (itemId: string) => {
|
||||
const toggleShowClaimant = (id: string) => {
|
||||
const newShowClaimants = new Set(showClaimants);
|
||||
if (newShowClaimants.has(itemId)) {
|
||||
newShowClaimants.delete(itemId);
|
||||
if (newShowClaimants.has(id)) {
|
||||
newShowClaimants.delete(id);
|
||||
} else {
|
||||
newShowClaimants.add(itemId);
|
||||
newShowClaimants.add(id);
|
||||
}
|
||||
setShowClaimants(newShowClaimants);
|
||||
};
|
||||
@ -89,50 +103,34 @@ const RegistryManager = () => {
|
||||
}
|
||||
|
||||
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="container mx-auto p-4">
|
||||
<form onSubmit={handleAddItem} className="mb-8">
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Add New Item</h2>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Item Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter item name"
|
||||
className="input input-bordered"
|
||||
value={newItem.name}
|
||||
onChange={(e) =>
|
||||
setNewItem({ ...newItem, name: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<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 className="flex flex-col gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.name}
|
||||
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||
placeholder="Item name"
|
||||
className="input input-bordered"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
value={newItem.link}
|
||||
onChange={(e) => setNewItem({ ...newItem, link: e.target.value })}
|
||||
placeholder="Item link (optional)"
|
||||
className="input input-bordered"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Add Item
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -140,7 +138,7 @@ const RegistryManager = () => {
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Name</th>
|
||||
<th>Link</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
@ -149,35 +147,7 @@ const RegistryManager = () => {
|
||||
<tbody>
|
||||
{registryItems.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<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>{item.name}</td>
|
||||
<td>
|
||||
{item.link && (
|
||||
<a
|
||||
@ -186,15 +156,38 @@ const RegistryManager = () => {
|
||||
rel="noopener noreferrer"
|
||||
className="link link-primary"
|
||||
>
|
||||
View
|
||||
View Item
|
||||
</a>
|
||||
)}
|
||||
</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>
|
||||
<button
|
||||
className="btn btn-error btn-sm"
|
||||
onClick={() => handleDeleteItem(item.id)}
|
||||
className="btn btn-error btn-sm"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { setAuthData } from "../utils/auth-client";
|
||||
|
||||
interface SignInProps {
|
||||
onSuccess?: () => void;
|
||||
@ -8,7 +9,7 @@ interface SignInProps {
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
role?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
||||
@ -16,16 +17,17 @@ const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (isDisabled) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Clear existing authentication before attempting new sign-in
|
||||
sessionStorage.removeItem("isAuthenticated");
|
||||
sessionStorage.removeItem("role");
|
||||
|
||||
const response = await fetch("/api/auth", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -36,21 +38,11 @@ const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
sessionStorage.setItem("isAuthenticated", "true");
|
||||
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);
|
||||
|
||||
setAuthData(data.token);
|
||||
onSuccess?.();
|
||||
} catch (err: any) {
|
||||
setError(err instanceof Error ? err.message : "Authentication failed");
|
||||
@ -89,7 +81,7 @@ const SignIn = ({ onSuccess, requiredRole = "guest" }: SignInProps) => {
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={!code || isSubmitting}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isSubmitting ? "Verifying..." : "Continue"}
|
||||
</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>
|
||||
function handleSignOut() {
|
||||
sessionStorage.removeItem("isAuthenticated");
|
||||
sessionStorage.removeItem("role");
|
||||
window.location.reload();
|
||||
}
|
||||
import { clearAuthData } from "../utils/auth-client";
|
||||
|
||||
// Add click handler to the button
|
||||
document.getElementById("signout-btn")?.addEventListener("click", () => {
|
||||
clearAuthData();
|
||||
});
|
||||
</script>
|
@ -1,30 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { clearAuthData, isAuthenticated } from "../utils/auth-client";
|
||||
|
||||
const SignOut = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const isAuthenticated = sessionStorage.getItem("isAuthenticated") === "true";
|
||||
setIsVisible(isAuthenticated);
|
||||
setIsVisible(isAuthenticated());
|
||||
|
||||
const handleAuthChange = () => {
|
||||
setIsVisible(sessionStorage.getItem("isAuthenticated") === "true");
|
||||
setIsVisible(isAuthenticated());
|
||||
};
|
||||
|
||||
document.addEventListener("auth-success", handleAuthChange);
|
||||
return () => document.removeEventListener("auth-success", handleAuthChange);
|
||||
}, []);
|
||||
|
||||
const handleSignOut = () => {
|
||||
sessionStorage.removeItem("isAuthenticated");
|
||||
sessionStorage.removeItem("role");
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<button onClick={handleSignOut} className="btn btn-secondary">
|
||||
<button onClick={clearAuthData} className="btn btn-secondary">
|
||||
Sign Out
|
||||
</button>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ interface 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" />
|
||||
</div>
|
||||
|
||||
@ -17,16 +17,24 @@ const { title } = Astro.props;
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check auth state on page load
|
||||
const isAuthenticated = sessionStorage.getItem("isAuthenticated") === "true";
|
||||
if (isAuthenticated) {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document.getElementById("content-container")?.classList.remove("hidden");
|
||||
import { isAuthenticated } from "../utils/auth-client";
|
||||
|
||||
function updateVisibility() {
|
||||
const authContainer = document.getElementById("auth-container");
|
||||
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
|
||||
document.addEventListener("auth-success", () => {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document.getElementById("content-container")?.classList.remove("hidden");
|
||||
});
|
||||
document.addEventListener("auth-success", updateVisibility);
|
||||
</script>
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import AuthLayout from "./AuthLayout.astro";
|
||||
import Navigation from "../components/Navigation.astro";
|
||||
|
||||
interface Props {
|
||||
@ -26,9 +25,7 @@ const { title } = Astro.props;
|
||||
<body>
|
||||
<Navigation />
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<AuthLayout title={title}>
|
||||
<slot />
|
||||
</AuthLayout>
|
||||
<slot />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { generateToken } from "../../utils/jwt";
|
||||
|
||||
interface AuthRequest {
|
||||
code: string;
|
||||
@ -15,11 +16,15 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const secretCode = process.env.SECRET_CODE || import.meta.env.SECRET_CODE;
|
||||
const adminCode = process.env.ADMIN_CODE || import.meta.env.ADMIN_CODE;
|
||||
|
||||
let role = "guest";
|
||||
let role: "guest" | "admin" | null = null;
|
||||
|
||||
if (code === adminCode) {
|
||||
role = "admin";
|
||||
} else if (code !== secretCode) {
|
||||
} else if (code === secretCode) {
|
||||
role = "guest";
|
||||
}
|
||||
|
||||
if (!role) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
@ -45,10 +50,19 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true, role }), {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
const token = generateToken(role);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
token,
|
||||
role
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Authentication failed:", error);
|
||||
return new Response(
|
||||
|
@ -1,11 +1,12 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getS3Data, putS3Data } from "../../../lib/s3";
|
||||
import type { RegistryItem } from "../../../lib/types";
|
||||
import { createProtectedAPIRoute } from "../../../utils/auth-middleware";
|
||||
|
||||
const REGISTRY_FILE_KEY = "registry.json";
|
||||
|
||||
// GET: Get a specific registry item by ID
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
// GET: Get a specific registry item by ID (requires guest role)
|
||||
const handleGet: APIRoute = async ({ params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
if (!id) {
|
||||
@ -38,8 +39,8 @@ export const GET: APIRoute = async ({ params }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// PUT: Update an existing registry item
|
||||
export const PUT: APIRoute = async ({ request, params }) => {
|
||||
// PUT: Update an existing registry item (requires guest role)
|
||||
const handlePut: APIRoute = async ({ request, params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
if (!id) {
|
||||
@ -83,9 +84,8 @@ export const PUT: APIRoute = async ({ request, params }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// DELETE: Delete a registry item
|
||||
export const DELETE: APIRoute = async ({ params }) => {
|
||||
// DELETE: Delete a registry item (requires admin role)
|
||||
const handleDelete: APIRoute = async ({ params }) => {
|
||||
const headers = {
|
||||
"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 { v4 as uuidv4 } from "uuid";
|
||||
import type { RegistryItem } from "../../../lib/types";
|
||||
import { createProtectedAPIRoute } from "../../../utils/auth-middleware";
|
||||
|
||||
const REGISTRY_FILE_KEY = "registry.json";
|
||||
|
||||
// GET: List all registry items
|
||||
export const GET: APIRoute = async () => {
|
||||
// GET: List all registry items (requires guest role)
|
||||
const handleGet: APIRoute = async () => {
|
||||
try {
|
||||
const registry = await getS3Data<RegistryItem[]>(REGISTRY_FILE_KEY) || [];
|
||||
return new Response(JSON.stringify(registry), {
|
||||
@ -22,8 +23,8 @@ export const GET: APIRoute = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// POST: Create a new registry item
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
// POST: Create a new registry item (requires admin role)
|
||||
const handlePost: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
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 { getS3Data, putS3Data } from "../../lib/s3";
|
||||
import type { RSVPItem } from "../../lib/types";
|
||||
import { createProtectedAPIRoute } from "../../utils/auth-middleware";
|
||||
|
||||
const objectsToCSV = (data: RSVPItem[]): string => {
|
||||
const headers = ["name", "dietaryRestrictions", "notes", "timestamp"];
|
||||
@ -44,8 +45,8 @@ const csvToObjects = (csv: string): RSVPItem[] => {
|
||||
});
|
||||
};
|
||||
|
||||
// GET: Retrieve all RSVPs
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
// GET: Retrieve all RSVPs (requires admin role)
|
||||
const handleGet: APIRoute = async ({ request }) => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
@ -81,8 +82,8 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// POST: Submit a new RSVP
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
// POST: Submit a new RSVP (requires guest role)
|
||||
const handlePost: APIRoute = async ({ request }) => {
|
||||
console.log("API endpoint hit - starting request processing");
|
||||
|
||||
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">
|
||||
<RegistryManager client:load />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
<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>
|
||||
</AdminLayout>
|
||||
|
||||
<script>
|
||||
const checkAndUpdateVisibility = (role: string | null) => {
|
||||
if (role === "admin") {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document
|
||||
.getElementById("manager-container")
|
||||
?.classList.remove("hidden");
|
||||
import { hasRole } from "../../utils/auth-client";
|
||||
|
||||
function updateVisibility() {
|
||||
const authContainer = document.getElementById("auth-container");
|
||||
const managerContainer = document.getElementById("manager-container");
|
||||
|
||||
if (hasRole("admin")) {
|
||||
authContainer?.classList.add("hidden");
|
||||
managerContainer?.classList.remove("hidden");
|
||||
} else {
|
||||
document
|
||||
.getElementById("auth-container")
|
||||
?.classList.remove("hidden");
|
||||
document
|
||||
.getElementById("manager-container")
|
||||
?.classList.add("hidden");
|
||||
authContainer?.classList.remove("hidden");
|
||||
managerContainer?.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Check auth state on page load
|
||||
const isAuthenticated =
|
||||
sessionStorage.getItem("isAuthenticated") === "true";
|
||||
const role = sessionStorage.getItem("role");
|
||||
checkAndUpdateVisibility(role);
|
||||
updateVisibility();
|
||||
|
||||
// Add event listener for custom event from SignIn component
|
||||
document.addEventListener("auth-success", ((event: CustomEvent) => {
|
||||
const newRole = event.detail?.role || sessionStorage.getItem("role");
|
||||
checkAndUpdateVisibility(newRole);
|
||||
}) as EventListener);
|
||||
document.addEventListener("auth-success", updateVisibility);
|
||||
</script>
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import RegistryList from "../../components/RegistryList.tsx";
|
||||
import SignIn from "../../components/SignIn.tsx";
|
||||
import SignOut from "../../components/SignOut.tsx";
|
||||
---
|
||||
|
||||
@ -10,37 +11,44 @@ import SignOut from "../../components/SignOut.tsx";
|
||||
View and Claim Items from the Registry:
|
||||
</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 />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
<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>
|
||||
// Check auth state on page load
|
||||
const isAuthenticated =
|
||||
sessionStorage.getItem("isAuthenticated") === "true";
|
||||
if (isAuthenticated) {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document
|
||||
.getElementById("registry-container")
|
||||
?.classList.remove("hidden");
|
||||
import { hasRole, isAuthenticated } from "../../utils/auth-client";
|
||||
|
||||
function updateVisibility() {
|
||||
const authContainer = document.getElementById("auth-container");
|
||||
const contentContainer = document.getElementById("content-container");
|
||||
|
||||
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
|
||||
document.addEventListener("auth-success", () => {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document
|
||||
.getElementById("registry-container")
|
||||
?.classList.remove("hidden");
|
||||
});
|
||||
document.addEventListener("auth-success", updateVisibility);
|
||||
|
||||
// Check for empty registry
|
||||
document.addEventListener("registry-empty", () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import RSVP from "../components/RSVP.tsx";
|
||||
import SignIn from "../components/SignIn.tsx";
|
||||
import SignOut from "../components/SignOut.tsx";
|
||||
---
|
||||
|
||||
@ -10,26 +11,39 @@ import SignOut from "../components/SignOut.tsx";
|
||||
Please RSVP using the form below:
|
||||
</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">
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
<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>
|
||||
// Check auth state on page load
|
||||
const isAuthenticated =
|
||||
sessionStorage.getItem("isAuthenticated") === "true";
|
||||
if (isAuthenticated) {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document.getElementById("rsvp-container")?.classList.remove("hidden");
|
||||
import { hasRole, isAuthenticated } from "../utils/auth-client";
|
||||
|
||||
function updateVisibility() {
|
||||
const authContainer = document.getElementById("auth-container");
|
||||
const contentContainer = document.getElementById("content-container");
|
||||
|
||||
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
|
||||
document.addEventListener("auth-success", () => {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document.getElementById("rsvp-container")?.classList.remove("hidden");
|
||||
});
|
||||
document.addEventListener("auth-success", updateVisibility);
|
||||
</script>
|
||||
|
@ -15,40 +15,33 @@ import SignOut from "../../components/SignOut.tsx";
|
||||
|
||||
<div id="manager-container" class="hidden">
|
||||
<RSVPManager client:load />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
<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>
|
||||
</AdminLayout>
|
||||
|
||||
<script>
|
||||
const checkAndUpdateVisibility = (role: string | null) => {
|
||||
if (role === "admin") {
|
||||
document.getElementById("auth-container")?.classList.add("hidden");
|
||||
document
|
||||
.getElementById("manager-container")
|
||||
?.classList.remove("hidden");
|
||||
import { hasRole } from "../../utils/auth-client";
|
||||
|
||||
function updateVisibility() {
|
||||
const authContainer = document.getElementById("auth-container");
|
||||
const managerContainer = document.getElementById("manager-container");
|
||||
|
||||
if (hasRole("admin")) {
|
||||
authContainer?.classList.add("hidden");
|
||||
managerContainer?.classList.remove("hidden");
|
||||
} else {
|
||||
document
|
||||
.getElementById("auth-container")
|
||||
?.classList.remove("hidden");
|
||||
document
|
||||
.getElementById("manager-container")
|
||||
?.classList.add("hidden");
|
||||
authContainer?.classList.remove("hidden");
|
||||
managerContainer?.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Check auth state on page load
|
||||
const isAuthenticated =
|
||||
sessionStorage.getItem("isAuthenticated") === "true";
|
||||
const role = sessionStorage.getItem("role");
|
||||
checkAndUpdateVisibility(role);
|
||||
updateVisibility();
|
||||
|
||||
// Add event listener for custom event from SignIn component
|
||||
document.addEventListener("auth-success", ((event: CustomEvent) => {
|
||||
const newRole = event.detail?.role || sessionStorage.getItem("role");
|
||||
checkAndUpdateVisibility(newRole);
|
||||
}) as EventListener);
|
||||
document.addEventListener("auth-success", updateVisibility);
|
||||
</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