Defer user-owned S3 setup until first media upload

Hop-State: A_06FP6DP9ENXP2WBSR2YPNA8
Hop-Proposal: R_06FP6DM6S5P96EFX90PH2F8
Hop-Task: T_06FP6C52Z3JHQBQ5XNNHZ3R
Hop-Attempt: AT_06FP6C52Z14V6CZJQTJN4PR
This commit is contained in:
2026-07-14 17:39:22 -07:00
committed by Hop
parent 0ecb622565
commit 752298a405
10 changed files with 304 additions and 274 deletions
+3 -42
View File
@@ -1,10 +1,8 @@
import { NextResponse } from 'next/server';
import { registerUser, createSession } from '@/lib/auth';
import { createStorageSession } from '@/lib/storage/session';
import { db, nodes, users } from '@/db';
import { db, users } from '@/db';
import { eq } from 'drizzle-orm';
import { verifyTurnstileToken } from '@/lib/turnstile';
import { testS3Credentials } from '@/lib/storage/s3';
import { z } from 'zod';
const registerSchema = z.object({
@@ -13,14 +11,6 @@ const registerSchema = z.object({
password: z.string().min(8),
displayName: z.string().optional(),
turnstileToken: z.string().nullable().optional(),
// S3-compatible storage credentials
storageProvider: z.string().min(1),
storageEndpoint: z.string().nullable().optional(),
storagePublicBaseUrl: z.string().nullable().optional(),
storageRegion: z.string().min(1),
storageBucket: z.string().min(1),
storageAccessKey: z.string().min(10),
storageSecretKey: z.string().min(10),
});
export async function POST(request: Request) {
@@ -28,8 +18,7 @@ export async function POST(request: Request) {
const body = await request.json();
// Log registration attempt (excluding password)
const { password, ...logData } = body;
console.log('Registration attempt details:', logData);
console.log('Registration attempt:', { handle: body.handle, email: body.email });
const data = registerSchema.parse(body);
@@ -46,38 +35,11 @@ export async function POST(request: Request) {
}
}
// Test S3 credentials before creating account
console.log('[Register] Testing S3 credentials...');
const s3Test = await testS3Credentials(
data.storageEndpoint || null,
data.storageRegion,
data.storageBucket,
data.storageAccessKey,
data.storageSecretKey
);
if (!s3Test.success) {
console.error('[Register] S3 credential test failed:', s3Test.error);
return NextResponse.json(
{ error: `Storage connection failed: ${s3Test.error}` },
{ status: 400 }
);
}
console.log('[Register] S3 credentials verified successfully');
const user = await registerUser(
data.handle,
data.email,
data.password,
data.displayName,
data.storageProvider,
data.storageEndpoint || null,
data.storagePublicBaseUrl || null,
data.storageRegion,
data.storageBucket,
data.storageAccessKey,
data.storageSecretKey
data.displayName
);
// Check if this is an NSFW node and auto-enable NSFW settings
@@ -98,7 +60,6 @@ export async function POST(request: Request) {
// Create session for new user
await createSession(user.id);
await createStorageSession(user, data.password);
return NextResponse.json({
success: true,
+3 -2
View File
@@ -44,8 +44,9 @@ export async function POST(req: NextRequest) {
// Check if user has S3 storage configured
if (!user.storageProvider || !user.storageAccessKeyEncrypted || !user.storageSecretKeyEncrypted) {
return NextResponse.json({
error: 'Storage not configured. Please set up S3-compatible storage in your settings.'
}, { status: 400 });
error: 'Connect your storage before uploading media.',
code: 'STORAGE_NOT_CONFIGURED',
}, { status: 409 });
}
const storageSession =
@@ -0,0 +1,73 @@
import { NextResponse } from 'next/server';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
import { db, users } from '@/db';
import { requireAuth, verifyPassword } from '@/lib/auth';
import { encryptPrivateKey, serializeEncryptedKey } from '@/lib/crypto/private-key';
import { testS3Credentials, type StorageProvider } from '@/lib/storage/s3';
import { createStorageSession } from '@/lib/storage/session';
const configurationSchema = z.object({
password: z.string().min(1),
provider: z.enum(['s3', 'r2', 'b2', 'wasabi', 'contabo']),
endpoint: z.string().trim().nullable().optional(),
publicBaseUrl: z.string().trim().nullable().optional(),
region: z.string().trim().min(2),
bucket: z.string().trim().min(3),
accessKey: z.string().min(10),
secretKey: z.string().min(10),
});
export async function POST(request: Request) {
try {
const user = await requireAuth();
const data = configurationSchema.parse(await request.json());
if (!user.passwordHash || !(await verifyPassword(data.password, user.passwordHash))) {
return NextResponse.json({ error: 'Incorrect account password' }, { status: 401 });
}
const storageTest = await testS3Credentials(
data.endpoint || null,
data.region,
data.bucket,
data.accessKey,
data.secretKey
);
if (!storageTest.success) {
return NextResponse.json(
{ error: `Storage connection failed: ${storageTest.error}` },
{ status: 400 }
);
}
const storageProvider = data.provider as StorageProvider;
const storageAccessKeyEncrypted = serializeEncryptedKey(encryptPrivateKey(data.accessKey, data.password));
const storageSecretKeyEncrypted = serializeEncryptedKey(encryptPrivateKey(data.secretKey, data.password));
const storageValues = {
storageProvider,
storageEndpoint: data.endpoint || null,
storagePublicBaseUrl: data.publicBaseUrl || null,
storageRegion: data.region,
storageBucket: data.bucket,
storageAccessKeyEncrypted,
storageSecretKeyEncrypted,
};
await db.update(users).set(storageValues).where(eq(users.id, user.id));
await createStorageSession({ ...user, ...storageValues }, data.password);
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid storage configuration', details: error.issues }, { status: 400 });
}
if (error instanceof Error && error.message === 'Authentication required') {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
console.error('Storage configuration error:', error);
return NextResponse.json({ error: 'Failed to configure storage' }, { status: 500 });
}
}