Fixed auth security

This commit is contained in:
2025-02-25 09:59:25 -06:00
parent 0be45ac39b
commit 4d2353b2c3
24 changed files with 681 additions and 350 deletions

View File

@ -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(

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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>

View File

@ -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", () => {

View File

@ -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>

View File

@ -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>

View 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>