Refactor registration and dashboard features to support invite codes and Bunny uploads

- Moved registration logic to a new client component for better separation of concerns.
- Integrated invite code requirement based on feature flags in the registration process.
- Enhanced dashboard functionality to conditionally enable Bunny uploads based on feature flags.
- Updated various components and API routes to check for Bunny uploads and Stripe billing feature flags.
- Added new feature flag utilities for managing feature toggles in the application.
This commit is contained in:
Yusuf İpek
2026-04-08 18:46:12 +03:00
parent 6f22b0bf8b
commit b1b1715578
29 changed files with 430 additions and 282 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import bcrypt from 'bcryptjs';
import { acceptInvitationTokenForUser, getValidInvitationByToken } from '@/lib/invitations';
import { checkRateLimit, getClientIp, rateLimitHeaders, RATE_LIMIT_CONFIGS } from '@/lib/rate-limit';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
import { isInviteCodeRequired } from '@/lib/feature-flags';
export async function POST(request: NextRequest) {
try {
@@ -49,7 +50,7 @@ export async function POST(request: NextRequest) {
}
}
if (!invitationIsValid) {
if (!invitationIsValid && isInviteCodeRequired()) {
// Validate invite code using constant-time comparison to prevent timing attacks
const validInviteCode = process.env.INVITE_CODE;
if (!validInviteCode || !inviteCode) {
+5
View File
@@ -7,6 +7,7 @@ import {
getStripeCheckoutState,
} from '@/lib/billing';
import { rateLimit } from '@/lib/rate-limit';
import { isStripeFeatureEnabled } from '@/lib/feature-flags';
import { getStripe, getStripePriceId, isStripeConfigured } from '@/lib/stripe';
import { isTrustedSameOriginRequest } from '@/lib/request-origin';
@@ -35,6 +36,10 @@ export async function POST(request: NextRequest) {
return apiErrors.unauthorized();
}
if (!isStripeFeatureEnabled()) {
return apiErrors.badRequest('Stripe billing is disabled by this host');
}
if (!isStripeConfigured()) {
return apiErrors.internalError('Stripe billing is not configured');
}
+5
View File
@@ -3,6 +3,7 @@ import { auth } from '@/lib/auth';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
import { getBillingOverview } from '@/lib/billing';
import { rateLimit } from '@/lib/rate-limit';
import { isStripeFeatureEnabled } from '@/lib/feature-flags';
import { getStripe, isStripeConfigured } from '@/lib/stripe';
import { isTrustedSameOriginRequest } from '@/lib/request-origin';
@@ -31,6 +32,10 @@ export async function POST(request: NextRequest) {
return apiErrors.unauthorized();
}
if (!isStripeFeatureEnabled()) {
return apiErrors.badRequest('Stripe billing is disabled by this host');
}
if (!isStripeConfigured()) {
return apiErrors.internalError('Stripe billing is not configured');
}
+7 -2
View File
@@ -1,7 +1,8 @@
import { auth } from '@/lib/auth';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
import { getBillingOverview } from '@/lib/billing';
import { isStripeConfigured } from '@/lib/stripe';
import { isStripeFeatureEnabled } from '@/lib/feature-flags';
import { hasStripeRuntimeConfig, isStripeConfigured } from '@/lib/stripe';
export async function GET() {
try {
@@ -11,8 +12,12 @@ export async function GET() {
}
const billing = await getBillingOverview(session.user.id);
const isEnabled = isStripeFeatureEnabled();
const isConfigured = hasStripeRuntimeConfig();
const response = successResponse({
isConfigured: isStripeConfigured(),
isEnabled,
isConfigured,
status: !isEnabled ? 'disabled' : isStripeConfigured() ? 'ready' : 'misconfigured',
checkoutAvailable: isStripeConfigured() && !billing.subscription.hasActiveSubscription,
portalAvailable: isStripeConfigured() && Boolean(billing.subscription.stripeCustomerId),
subscription: {
@@ -6,6 +6,7 @@ import { rateLimit } from '@/lib/rate-limit';
import crypto from 'crypto';
import { cleanupBunnyStreamVideos } from '@/lib/bunny-stream-cleanup';
import { createBunnyUploadToken, verifyBunnyUploadToken } from '@/lib/bunny-upload-token';
import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags';
type RouteParams = { params: Promise<{ projectId: string }> };
@@ -50,6 +51,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
return apiErrors.badRequest('Title is required');
}
if (!isBunnyUploadsFeatureEnabled()) {
return apiErrors.badRequest('Direct uploads are disabled by this host');
}
const apiKey = process.env.BUNNY_STREAM_API_KEY;
const libraryId = process.env.BUNNY_STREAM_LIBRARY_ID || process.env.NEXT_PUBLIC_BUNNY_STREAM_LIBRARY_ID;
@@ -10,6 +10,7 @@ import {
enforceGuestUploadQuota,
verifyGuestUploadToken,
} from '@/lib/guest-upload-token';
import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags';
import { getShareSessionFromRequest } from '@/lib/share-session';
import { getVideoAssetAccessContext, SAFE_BUNNY_VIDEO_ID } from '@/lib/video-assets';
@@ -30,6 +31,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
const title = typeof body?.title === 'string' ? body.title.trim() : '';
if (!title) return apiErrors.badRequest('Title is required');
if (!isBunnyUploadsFeatureEnabled()) {
return apiErrors.badRequest('Direct uploads are disabled by this host');
}
const shareSession = getShareSessionFromRequest(request, context.video.id);
if (!context.viewerUserId) {
const quotaError = await enforceGuestUploadQuota(request, context.video.id, 'bunny', shareSession?.token ?? null);