Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
# VCS / CI
.git
.github

# Build output
.next
dist
out
coverage

# Dependencies
node_modules

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
bun-debug.log*

# Local env (keep .env.example)
.env
.env.*
!.env.example
Dockerfile
README.md

# Tooling caches
tsconfig.tsbuildinfo
.turbo
.cache

# OS/editor junk
.DS_Store
.vscode
8 changes: 7 additions & 1 deletion .github/workflows/fly-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
- run: |
flyctl deploy --remote-only --verbose \
--build-arg NEXT_PUBLIC_SUPABASE_URL="${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" \
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" \
--build-arg NEXT_PUBLIC_REOWN_PROJECT_ID="${{ secrets.NEXT_PUBLIC_REOWN_PROJECT_ID }}" \
--build-arg NEXT_PUBLIC_ADMIN_ADDRESS="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESS }}" \
--build-arg NEXT_PUBLIC_ADMIN_ADDRESSES="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESSES }}"
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
23 changes: 23 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: PR Build

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

jobs:
build:
name: Build Docker image (PR)
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4

- name: Docker build (validates bun run build in Dockerfile)
run: |
docker build \
--build-arg NEXT_PUBLIC_SUPABASE_URL="${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" \
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" \
--build-arg NEXT_PUBLIC_REOWN_PROJECT_ID="${{ secrets.NEXT_PUBLIC_REOWN_PROJECT_ID }}" \
--build-arg NEXT_PUBLIC_ADMIN_ADDRESS="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESS }}" \
--build-arg NEXT_PUBLIC_ADMIN_ADDRESSES="${{ secrets.NEXT_PUBLIC_ADMIN_ADDRESSES }}" \
-t web3copy:pr-${{ github.event.pull_request.number }} .
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ COPY . .
# inlines NEXT_PUBLIC_* values into the client bundle during next build.
ARG NEXT_PUBLIC_ADMIN_ADDRESS
ARG NEXT_PUBLIC_ADMIN_ADDRESSES
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ARG NEXT_PUBLIC_REOWN_PROJECT_ID

ENV NEXT_PUBLIC_ADMIN_ADDRESS="${NEXT_PUBLIC_ADMIN_ADDRESS}"
ENV NEXT_PUBLIC_ADMIN_ADDRESSES="${NEXT_PUBLIC_ADMIN_ADDRESSES}"
ENV NEXT_PUBLIC_SUPABASE_URL="${NEXT_PUBLIC_SUPABASE_URL}"
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY}"
ENV NEXT_PUBLIC_REOWN_PROJECT_ID="${NEXT_PUBLIC_REOWN_PROJECT_ID}"

# Build application (runs prisma generate + prisma:sync + next build via package.json)
RUN bun run build
Expand Down
1 change: 1 addition & 0 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSignMessage } from "wagmi";
import { Button } from "@/components/ui/button";
import { WalletButton } from "@/components/wallet-button";

export const dynamic = "force-dynamic";
export default function AdminPage() {
const { address, isConnected, mounted } = useWallet();
const { signMessageAsync } = useSignMessage();
Expand Down
5 changes: 3 additions & 2 deletions src/app/api/admin/approve/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getPrisma } from "@/lib/prisma";
import { setName } from "@/lib/namestone";
import { verifyMessage } from "viem";
import { isAllowedAdminAddress } from "@/lib/admin-auth";
Expand Down Expand Up @@ -28,8 +28,9 @@ export async function POST(req: NextRequest) {
const isAuth = await verifyAdminAuth(req);
if (!isAuth)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

try {
const prisma = getPrisma();
const { claimId } = await req.json();

const claim = await prisma.claimRequest.findUnique({
Expand Down
6 changes: 5 additions & 1 deletion src/app/api/admin/claims/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getPrisma } from "@/lib/prisma";
import { verifyMessage } from "viem";
import { isAllowedAdminAddress } from "@/lib/admin-auth";

Expand Down Expand Up @@ -36,7 +36,9 @@ export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const status = searchParams.get("status");


try {
const prisma = getPrisma();
const claims = await prisma.claimRequest.findMany({
where: status ? { status } : undefined,
orderBy: { createdAt: "desc" },
Expand All @@ -56,7 +58,9 @@ export async function PUT(req: NextRequest) {
if (!isAuth)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });


try {
const prisma = getPrisma();
const body = await req.json();
const { id, status } = body; // status can be "APPROVED", "REJECTED"

Expand Down
5 changes: 4 additions & 1 deletion src/app/api/admin/revoke/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getPrisma } from "@/lib/prisma";
import { deleteName } from "@/lib/namestone";
import { verifyMessage } from "viem";
import { isAllowedAdminAddress } from "@/lib/admin-auth";
Expand Down Expand Up @@ -29,7 +29,10 @@ export async function POST(req: NextRequest) {
if (!isAuth)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });



try {
const prisma = getPrisma();
const { name, claimId } = await req.json();

// Delete from NameStone
Expand Down
6 changes: 5 additions & 1 deletion src/app/api/claims/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getPrisma } from "@/lib/prisma";

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
Expand All @@ -9,7 +9,9 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: "Address is required" }, { status: 400 });
}


try {
const prisma = getPrisma();
const claims = await prisma.claimRequest.findMany({
where: {
walletAddress: address.toLowerCase(),
Expand All @@ -29,7 +31,9 @@ export async function GET(req: NextRequest) {
}

export async function POST(req: NextRequest) {

try {
const prisma = getPrisma();
const body = await req.json();
const { walletAddress, requestedName } = body;
const normalizedWalletAddress = walletAddress?.toLowerCase();
Expand Down
30 changes: 17 additions & 13 deletions src/lib/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const normalizeDatabaseUrl = (value?: string) => {
return value.trim().replace(/^['\"]+|['\"]+$/g, "");
};

const prismaClientSingleton = () => {
const createPrismaClient = () => {
const databaseUrl = normalizeDatabaseUrl(process.env.DATABASE_URL);

if (!databaseUrl) {
Expand All @@ -24,21 +24,25 @@ const prismaClientSingleton = () => {
}

if (protocol !== "postgresql:" && protocol !== "postgres:") {
throw new Error(
"DATABASE_URL must start with postgresql:// or postgres://",
);
throw new Error("DATABASE_URL must start with postgresql:// or postgres://");
}

const pool = new Pool({ connectionString: databaseUrl });
const adapter = new PrismaPg(pool);
const client = new PrismaClient({ adapter });
return client;
return new PrismaClient({ adapter });
};

declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();

if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
declare global {
// eslint-disable-next-line no-var
var prismaGlobal: PrismaClient | undefined;
}

/**
* Lazy Prisma accessor.
* Safe to import during `next build` because it does not read env / connect
* until you call it inside a request handler.
*/
export const getPrisma = (): PrismaClient => {
globalThis.prismaGlobal ??= createPrismaClient();
return globalThis.prismaGlobal;
};
24 changes: 14 additions & 10 deletions src/services/registrations/registrations-service.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { RegistrationData } from "../../lib/schemas/registration";
import { supabase } from "../supabase";
import { getSupabase } from "../supabase";

const PROFILE_PICTURE_BUCKET = "profile_pictures";

export const dynamic = "force-dynamic";
export const RegistrationService = {
submitRegistration: async (registrationData: RegistrationData) => {
const supabase = getSupabase();
const { data, error } = await supabase
.from("registrations")
.insert([registrationData])
Expand All @@ -19,7 +20,7 @@ export const RegistrationService = {

isEmailTaken: async (email: string) => {
const cleanEmail = email.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase.rpc("is_email_registered", {
search_email: cleanEmail,
});
Expand All @@ -33,7 +34,7 @@ export const RegistrationService = {

isWalletEmpty: async (email: string) => {
const cleanEmail = email.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase.rpc("is_wallet_id_empty", {
input_email: cleanEmail,
});
Expand All @@ -47,7 +48,7 @@ export const RegistrationService = {

isWalletRegistered: async (walletAddress: string) => {
const cleanWalletAddress = walletAddress.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase.rpc("is_wallet_registered", {
search_wallet: cleanWalletAddress,
});
Expand All @@ -62,7 +63,7 @@ export const RegistrationService = {
linkWalletToEmail: async (email: string, walletAddress: string) => {
const cleanEmail = email.trim().toLowerCase();
const cleanWalletAddress = walletAddress.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase.rpc("link_wallet_to_email", {
input_email: cleanEmail,
input_wallet: cleanWalletAddress,
Expand Down Expand Up @@ -90,11 +91,12 @@ export const RegistrationService = {
},

uploadProfilePicture: async (email: string, file: File) => {
const supabase = getSupabase();
const profilePicturePath = RegistrationService.getProfilePicturePath(
email,
file,
);

const { error } = await supabase.storage
.from(PROFILE_PICTURE_BUCKET)
.upload(profilePicturePath, file, {
Expand Down Expand Up @@ -128,6 +130,7 @@ export const RegistrationService = {
profilePicturePath?: string;
profilePictureUrl?: string;
}) => {
const supabase = getSupabase();
const cleanEmail = email.trim().toLowerCase();

const updateData: {
Expand Down Expand Up @@ -163,6 +166,7 @@ export const RegistrationService = {
},

getRegistrationByWallet: async (walletAddress: string) => {
const supabase = getSupabase();
const cleanWalletAddress =
RegistrationService.normalizeWalletAddress(walletAddress);

Expand Down Expand Up @@ -251,7 +255,7 @@ export const RegistrationService = {

getRegistrationByEmail: async (email: string) => {
const cleanEmail = email.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase
.from("registrations")
.select("*")
Expand All @@ -267,7 +271,7 @@ export const RegistrationService = {

updateBadges: async (email: string, badges: string[]) => {
const cleanEmail = email.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase
.from("registrations")
.update({
Expand Down Expand Up @@ -332,7 +336,7 @@ export const RegistrationService = {

updateEventsAttended: async (email: string, eventsAttended: string[]) => {
const cleanEmail = email.trim().toLowerCase();

const supabase = getSupabase();
const { data, error } = await supabase
.from("registrations")
.update({
Expand Down
14 changes: 9 additions & 5 deletions src/services/supabase.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createClient } from "@supabase/supabase-js";
import { createClient, SupabaseClient } from "@supabase/supabase-js";
let _supabase: SupabaseClient | null = null;

/**
* Creates and exports a Supabase client instance.
Expand All @@ -7,17 +8,20 @@ import { createClient } from "@supabase/supabase-js";
*
* @see https://supabase.com/docs/client/imports
*/
export const supabase = (() => {
export function getSupabase(): SupabaseClient {
if (_supabase) return _supabase;

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim();
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim();

if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
"Missing Supabase environment variables " +
(supabaseUrl ? "" : "NEXT_PUBLIC_SUPABASE_URL") +
(supabaseUrl ? "" : "NEXT_PUBLIC_SUPABASE_URL ") +
(supabaseAnonKey ? "" : "NEXT_PUBLIC_SUPABASE_ANON_KEY"),
);
}

return createClient(supabaseUrl, supabaseAnonKey);
})();
_supabase = createClient(supabaseUrl, supabaseAnonKey);
return _supabase;
}
Loading