mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-03-21 17:54:09 -05:00
Use public get urls for hack cover images
This commit is contained in:
parent
3888e9e26d
commit
0b355fd93c
15
README.md
15
README.md
|
|
@ -16,7 +16,7 @@ Hackdex is a community hub for discovering and sharing Pokémon romhack patches.
|
|||
- **Discover**: curated hacks with screenshots, tags, versions, and summaries
|
||||
- **Submit**: metadata, screenshots, social links, and a BPS patch file
|
||||
- **Patch in the browser**: Powered by [RomPatcher.js](https://github.com/marcrobledo/RomPatcher.js); linked base roms stay on the user's device
|
||||
- **Safe delivery**: short-lived signed URLs for assets and downloads; no rom storage required
|
||||
- **Safe delivery**: public urls for cover images, short-lived signed URLs for patch downloads and other assets; no rom storage required
|
||||
|
||||
## Tech stack
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ Hackdex is a community hub for discovering and sharing Pokémon romhack patches.
|
|||
## High-level architecture
|
||||
|
||||
- UI: Next.js App Router with a mix of server and client components
|
||||
- Data: Supabase tables for hacks, tags, covers, patches; signed URLs for `hack-covers`
|
||||
- Data: Supabase tables for hacks, tags, patches; cover images stored in `covers` S3-compatible bucket
|
||||
- Patches: stored in an S3-compatible bucket `patches`; downloads use short-lived signed URLs via an API route
|
||||
- Auth: Supabase SSR helpers manage cookies; client SDK for browser calls
|
||||
|
||||
|
|
@ -66,6 +66,8 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
|
|||
NEXT_PUBLIC_SITE_URL=
|
||||
NEXT_PUBLIC_SITE_DOMAIN=
|
||||
|
||||
NEXT_PUBLIC_HACK_COVERS_DOMAIN=
|
||||
|
||||
S3_ENDPOINT=
|
||||
S3_PORT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
|
|
@ -84,12 +86,15 @@ Typical flow:
|
|||
1) Initialize and start services using the CLI
|
||||
2) Note the printed API URL and publishable key; set them in `.env.local` as shown above
|
||||
3) Apply this repository’s migrations in `supabase/migrations`
|
||||
4) Create a Supabase Storage bucket named `hack-covers` (private is fine; the app uses signed URLs)
|
||||
4) Create a Supabase Storage bucket named `hack-covers` and make it public. Set the `NEXT_PUBLIC_HACK_COVERS_DOMAIN` environment variable to the public URL of your covers bucket (e.g., `https://your-project.supabase.co/storage/v1/object/public/hack-covers`)
|
||||
|
||||
### S3‑compatible storage for patches
|
||||
### S3‑compatible storage for patches and covers
|
||||
|
||||
- Create a bucket named `patches`
|
||||
- Create a bucket named `patches` and a public bucket named `covers`
|
||||
- Point the `S3_*` environment variables to your S3 endpoint (Minio locally or your vendor)
|
||||
- Set the `COVERS_BUCKET` environment variable to the name of your covers bucket (e.g., `covers`)
|
||||
- Set the `PATCHES_BUCKET` environment variable to the name of your patches bucket (e.g., `patches`)
|
||||
- Set the `NEXT_PUBLIC_HACK_COVERS_DOMAIN` environment variable to the public URL of your covers bucket (e.g., `http://localhost:9000/covers`)
|
||||
|
||||
### Install & run
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import HackForm from "@/components/Hack/HackForm";
|
|||
import { createClient } from "@/utils/supabase/server";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";
|
||||
import Link from "next/link";
|
||||
import { getCoverSignedUrls } from "@/app/hack/actions";
|
||||
import { getCoverUrls } from "@/utils/format";
|
||||
import { checkEditPermission } from "@/utils/hack";
|
||||
|
||||
interface EditPageProps {
|
||||
|
|
@ -42,7 +42,7 @@ export default async function EditHackPage({ params }: EditPageProps) {
|
|||
.order("position", { ascending: true });
|
||||
if (covers && covers.length > 0) {
|
||||
coverKeys = covers.map((c: any) => c.url);
|
||||
signedCoverUrls = await getCoverSignedUrls(coverKeys);
|
||||
signedCoverUrls = getCoverUrls(coverKeys);
|
||||
}
|
||||
|
||||
const { data: tagRows } = await supabase
|
||||
|
|
|
|||
|
|
@ -18,9 +18,8 @@ import serialize from "serialize-javascript";
|
|||
import { headers } from "next/headers";
|
||||
import { MenuItem } from "@headlessui/react";
|
||||
import { FaCircleCheck } from "react-icons/fa6";
|
||||
import { sortOrderedTags } from "@/utils/format";
|
||||
import { sortOrderedTags, getCoverUrls } from "@/utils/format";
|
||||
import { RiArchiveStackFill } from "react-icons/ri";
|
||||
import { getCoverSignedUrls } from "@/app/hack/actions";
|
||||
import { isInformationalArchiveHack, isDownloadableArchiveHack, isArchiveHack, checkEditPermission } from "@/utils/hack";
|
||||
import Avatar from "@/components/Account/Avatar";
|
||||
|
||||
|
|
@ -132,7 +131,7 @@ export default async function HackDetail({ params }: HackDetailProps) {
|
|||
.eq("hack_slug", slug)
|
||||
.order("position", { ascending: true });
|
||||
if (covers && covers.length > 0) {
|
||||
images = await getCoverSignedUrls(covers.map(c => c.url));
|
||||
images = getCoverUrls(covers.map(c => c.url));
|
||||
}
|
||||
|
||||
const { data: tagRows } = await supabase
|
||||
|
|
|
|||
|
|
@ -265,21 +265,6 @@ export async function presignCoverUpload(args: { slug: string; objectKey: string
|
|||
return { ok: true, presignedUrl: url } as const;
|
||||
}
|
||||
|
||||
export async function getCoverSignedUrl(objectKey: string) {
|
||||
const client = getMinioClient();
|
||||
// 5 minutes expiry for viewing
|
||||
const url = await client.presignedGetObject(COVERS_BUCKET, objectKey, 60 * 5);
|
||||
return url;
|
||||
}
|
||||
|
||||
export async function getCoverSignedUrls(objectKeys: string[]) {
|
||||
const client = getMinioClient();
|
||||
// 5 minutes expiry for viewing
|
||||
const urls = await Promise.all(
|
||||
objectKeys.map(key => client.presignedGetObject(COVERS_BUCKET, key, 60 * 5))
|
||||
);
|
||||
return urls;
|
||||
}
|
||||
|
||||
export async function approveHack(slug: string) {
|
||||
const supabase = await createClient();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { FaArrowRightLong } from "react-icons/fa6";
|
|||
import { createClient } from "@/utils/supabase/server";
|
||||
import HackCard from "@/components/HackCard";
|
||||
import Button from "@/components/Button";
|
||||
import { sortOrderedTags } from "@/utils/format";
|
||||
import { getCoverSignedUrls } from "@/app/hack/actions";
|
||||
import { sortOrderedTags, getCoverUrls } from "@/utils/format";
|
||||
import { HackCardAttributes } from "@/components/HackCard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -42,7 +41,7 @@ export default async function Home() {
|
|||
|
||||
const coversBySlug = new Map<string, string[]>();
|
||||
if (coverRows && coverRows.length > 0) { const coverKeys = coverRows.map((c) => c.url);
|
||||
const signedUrls = await getCoverSignedUrls(coverKeys);
|
||||
const signedUrls = getCoverUrls(coverKeys);
|
||||
const urlToSignedUrl = new Map<string, string>();
|
||||
coverKeys.forEach((key, idx) => {
|
||||
urlToSignedUrl.set(key, signedUrls[idx]);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import { MdTune } from "react-icons/md";
|
|||
import { BsSdCardFill } from "react-icons/bs";
|
||||
import { CATEGORY_ICONS } from "@/components/Icons/tagCategories";
|
||||
import { useBaseRoms } from "@/contexts/BaseRomContext";
|
||||
import { sortOrderedTags, OrderedTag } from "@/utils/format";
|
||||
import { getCoverSignedUrls } from "@/app/hack/actions";
|
||||
import { sortOrderedTags, OrderedTag, getCoverUrls } from "@/utils/format";
|
||||
import { HackCardAttributes } from "@/components/HackCard";
|
||||
|
||||
const HACKS_PER_PAGE = 9;
|
||||
|
|
@ -82,7 +81,7 @@ export default function DiscoverBrowser() {
|
|||
const coversBySlug = new Map<string, string[]>();
|
||||
if (coverRows && coverRows.length > 0) {
|
||||
const coverKeys = coverRows.map(c => c.url);
|
||||
const urls = await getCoverSignedUrls(coverKeys);
|
||||
const urls = getCoverUrls(coverKeys);
|
||||
// Map: storage object url -> signedUrl
|
||||
const urlToSignedUrl = new Map<string, string>();
|
||||
coverKeys.forEach((key, idx) => {
|
||||
|
|
|
|||
|
|
@ -31,3 +31,25 @@ export function slugify(text: string) {
|
|||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public URL for a single cover image
|
||||
*/
|
||||
export function getCoverUrl(objectKey: string): string {
|
||||
const domain = process.env.NEXT_PUBLIC_HACK_COVERS_DOMAIN;
|
||||
if (!domain) {
|
||||
throw new Error("NEXT_PUBLIC_HACK_COVERS_DOMAIN environment variable is not set");
|
||||
}
|
||||
return `${domain}/${objectKey}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public URLs for multiple cover images
|
||||
*/
|
||||
export function getCoverUrls(objectKeys: string[]): string[] {
|
||||
const domain = process.env.NEXT_PUBLIC_HACK_COVERS_DOMAIN;
|
||||
if (!domain) {
|
||||
throw new Error("NEXT_PUBLIC_HACK_COVERS_DOMAIN environment variable is not set");
|
||||
}
|
||||
return objectKeys.map(key => `${domain}/${key}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user