Skip to content
Open
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
11 changes: 11 additions & 0 deletions app/(builder)/ycode/api/api-keys/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';
import { getApiKeyById, deleteApiKey } from '@/lib/repositories/apiKeyRepository';
import { noCache } from '@/lib/api-response';
import { getAdminUser } from '@/lib/supabase-auth';

// Disable caching for this route
export const dynamic = 'force-dynamic';
Expand All @@ -15,6 +16,11 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;
const key = await getApiKeyById(id);

Expand Down Expand Up @@ -46,6 +52,11 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;

// Verify the key exists first
Expand Down
11 changes: 11 additions & 0 deletions app/(builder)/ycode/api/api-keys/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';
import { getAllApiKeys, createApiKey } from '@/lib/repositories/apiKeyRepository';
import { noCache } from '@/lib/api-response';
import { getAdminUser } from '@/lib/supabase-auth';

// Disable caching for this route
export const dynamic = 'force-dynamic';
Expand All @@ -12,6 +13,11 @@ export const revalidate = 0;
*/
export async function GET() {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const keys = await getAllApiKeys();

return noCache({
Expand Down Expand Up @@ -48,6 +54,11 @@ export async function GET() {
*/
export async function POST(request: NextRequest) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const body = await request.json();
const { name } = body;

Expand Down
17 changes: 17 additions & 0 deletions app/(builder)/ycode/api/asset-folders/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server';
import { deleteAssetFolder, updateAssetFolder, getAssetFolderById } from '@/lib/repositories/assetFolderRepository';
import { getAdminUser } from '@/lib/supabase-auth';
import { noCache } from '@/lib/api-response';

// Disable caching for this route
Expand All @@ -16,6 +17,11 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;

const folder = await getAssetFolderById(id);
Expand Down Expand Up @@ -51,7 +57,13 @@ export async function PUT(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;

const body = await request.json();

// Validate required fields if provided
Expand Down Expand Up @@ -88,6 +100,11 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;

// Delete the folder and all its contents
Expand Down
11 changes: 11 additions & 0 deletions app/(builder)/ycode/api/asset-folders/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server';
import { getAllAssetFolders, createAssetFolder } from '@/lib/repositories/assetFolderRepository';
import { getAdminUser } from '@/lib/supabase-auth';
import { noCache } from '@/lib/api-response';

// Disable caching for this route
Expand All @@ -13,6 +14,11 @@ export const revalidate = 0;
*/
export async function GET() {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const folders = await getAllAssetFolders();

return noCache({
Expand All @@ -35,6 +41,11 @@ export async function GET() {
*/
export async function POST(request: NextRequest) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const body = await request.json();
const { name, asset_folder_id = null, depth = 0, order = 0 } = body;

Expand Down
11 changes: 11 additions & 0 deletions app/(builder)/ycode/api/assets/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAdminUser } from '@/lib/supabase-auth';
import { getAllAssets, getAssetsPaginated, createAsset } from '@/lib/repositories/assetRepository';
import { uploadFile, cleanSvgContent, isValidSvg } from '@/lib/file-upload';
import { noCache } from '@/lib/api-response';
Expand All @@ -20,6 +21,11 @@ export const revalidate = 0;
*/
export async function GET(request: NextRequest) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { searchParams } = new URL(request.url);
const folderIdParam = searchParams.get('folderId');
const folderIdsParam = searchParams.get('folderIds');
Expand Down Expand Up @@ -72,6 +78,11 @@ export async function GET(request: NextRequest) {
*/
export async function POST(request: NextRequest) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const contentType = request.headers.get('content-type');

// Handle JSON request for SVG creation
Expand Down
14 changes: 14 additions & 0 deletions app/(builder)/ycode/api/auth/invite/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';
import { getSupabaseAdmin } from '@/lib/supabase-server';
import { noCache } from '@/lib/api-response';
import { AUTH_ROLES } from '@/lib/auth-constants';

/**
* POST /ycode/api/auth/invite
Expand Down Expand Up @@ -53,6 +54,19 @@ export async function POST(request: NextRequest) {
);
}

if (data.user) {
// Promote to admin in app_metadata (required for builder access)
const { error: updateError } = await client.auth.admin.updateUserById(
data.user.id,
{ app_metadata: { role: AUTH_ROLES.ADMIN } }
);

if (updateError) {
console.error('[invite] Error promoting user to admin:', updateError);
// We don't fail the whole request since the user was created/invited
}
}

return noCache({
data: {
user: data.user,
Expand Down
19 changes: 18 additions & 1 deletion app/(builder)/ycode/api/auth/users/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { getSupabaseAdmin } from '@/lib/supabase-server';
import { noCache } from '@/lib/api-response';
import { getAdminUser } from '@/lib/supabase-auth';

/**
* GET /ycode/api/auth/users
Expand All @@ -9,6 +10,11 @@ import { noCache } from '@/lib/api-response';
*/
export async function GET(request: NextRequest) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}

const client = await getSupabaseAdmin();

if (!client) {
Expand Down Expand Up @@ -49,6 +55,12 @@ export async function GET(request: NextRequest) {
}> = [];

for (const user of data.users) {
// Check for the admin role - only admins/editors should be in this list
const role = user.app_metadata?.role;
if (role !== 'admin') {
continue;
}

// Get metadata - check both user_metadata and raw_user_meta_data

const userAny = user as any;
Expand Down Expand Up @@ -108,6 +120,11 @@ export async function GET(request: NextRequest) {
*/
export async function DELETE(request: NextRequest) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}

const { searchParams } = new URL(request.url);
const userId = searchParams.get('id');

Expand Down
11 changes: 11 additions & 0 deletions app/(builder)/ycode/api/collections/[id]/fields/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getFieldsByCollectionId, createField, getFieldById } from '@/lib/repositories/collectionFieldRepository';
import { isValidFieldType, VALID_FIELD_TYPES } from '@/lib/collection-field-utils';
import { getAdminUser } from '@/lib/supabase-auth';
import { noCache } from '@/lib/api-response';

// Disable caching for this route
Expand All @@ -18,6 +19,11 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;

// Extract search query parameter
Expand Down Expand Up @@ -48,6 +54,11 @@ export async function POST(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const adminAuth = await getAdminUser();
if (!adminAuth) {
return noCache({ error: 'Not authenticated' }, 401);
}

const { id } = await params;

const body = await request.json();
Expand Down
71 changes: 64 additions & 7 deletions app/(builder)/ycode/api/collections/[id]/items/filter/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest } from 'next/server';
import { getSupabaseAdmin } from '@/lib/supabase-server';
import { getSiteUser } from '@/lib/supabase-auth';
import { getItemsByCollectionId } from '@/lib/repositories/collectionItemRepository';
import { getValuesByItemIds } from '@/lib/repositories/collectionItemValueRepository';
import { getFieldsByCollectionId } from '@/lib/repositories/collectionFieldRepository';
Expand Down Expand Up @@ -418,31 +419,83 @@ async function getFilteredItemIds(
collectionId: string,
isPublished: boolean,
filterGroups: FilterCondition[][],
userScope?: boolean,
userScopeFieldId?: string,
): Promise<{ matchingIds: string[]; total: number }> {
const client = await getSupabaseAdmin();
if (!client) throw new Error('Supabase client not configured');

// 1. Resolve current user if needed (either for explicit 'current_user' filter or global userScope)
let currentUserId: string | null = null;
const hasExplicitUserFilter = filterGroups.some(g => g.some(c => c.value === 'current_user' || c.value2 === 'current_user'));

if (userScope || hasExplicitUserFilter) {
const siteAuth = await getSiteUser();
currentUserId = siteAuth?.user?.id || null;
}

// 2. Fetch all base item IDs for the collection
const allItemIds = await getAllItemIdsForCollection(client, collectionId, isPublished);
if (allItemIds.length === 0) return { matchingIds: [], total: 0 };

let baseIds = new Set(allItemIds);

// 3. Apply Global User Scope if enabled
if (userScope) {
let targetFieldId = userScopeFieldId;

// If no specific field ID provided, look for 'supabase_user_id' key in this collection
if (!targetFieldId) {
const fields = await getFieldsByCollectionId(collectionId, isPublished);
const userField = fields.find(f => f.key === 'supabase_user_id');
if (userField) targetFieldId = userField.id;
}

if (targetFieldId) {
const userFilter: FilterCondition = {
fieldId: targetFieldId,
operator: 'is',
value: currentUserId || 'guest', // 'guest' ensures no matches if logged out
};
const matchingForUser = await getIdsMatchingFilter(client, userFilter, isPublished, [...baseIds]);
baseIds = new Set([...baseIds].filter(id => matchingForUser.has(id)));
}

if (baseIds.size === 0) return { matchingIds: [], total: 0 };
}

// 4. Apply standard filter groups (if any)
if (filterGroups.length === 0) {
return { matchingIds: allItemIds, total: allItemIds.length };
const matchingIds = Array.from(baseIds);
return { matchingIds, total: matchingIds.length };
}

// Each group's conditions are ANDed. Groups are ORed (union).
const groupResults: Set<string>[] = [];

for (const group of filterGroups) {
let currentIds = new Set(allItemIds);
let currentIds = new Set(baseIds);

for (let filter of group) {
for (const filter of group) {
if (currentIds.size === 0) break;
if (isDateFieldType(filter.fieldType) && isDatePreset(filter.value)) {
const resolved = resolveDateFilterValue(filter.operator, filter.value, filter.value2);
// Resolve "current_user" placeholder in explicit filters
let filterValue = filter.value;
let filterValue2 = filter.value2;

if (filterValue === 'current_user') filterValue = currentUserId || 'guest';
if (filterValue2 === 'current_user') filterValue2 = currentUserId || 'guest';

const resolvedFilter = { ...filter, value: filterValue, value2: filterValue2 };

if (isDateFieldType(resolvedFilter.fieldType) && isDatePreset(resolvedFilter.value)) {
const resolved = resolveDateFilterValue(resolvedFilter.operator, resolvedFilter.value, resolvedFilter.value2);
if (resolved) {
filter = { ...filter, operator: resolved.operator, value: resolved.value, value2: resolved.value2 };
resolvedFilter.operator = resolved.operator;
resolvedFilter.value = resolved.value;
resolvedFilter.value2 = resolved.value2;
}
}
const matchingForFilter = await getIdsMatchingFilter(client, filter, isPublished, [...currentIds]);
const matchingForFilter = await getIdsMatchingFilter(client, resolvedFilter, isPublished, [...currentIds]);
currentIds = new Set([...currentIds].filter(id => matchingForFilter.has(id)));
}

Expand Down Expand Up @@ -522,6 +575,8 @@ export async function POST(
layerTemplate,
collectionLayerId,
filterGroups = [],
userScope,
userScopeFieldId,
sortBy,
sortOrder = 'asc',
limit,
Expand All @@ -543,6 +598,8 @@ export async function POST(
collectionId,
isPublished,
filterGroups,
userScope,
userScopeFieldId,
);

if (matchingIds.length === 0) {
Expand Down
Loading