Make user media uploads session-backed

This commit is contained in:
cyph3rasi
2026-03-07 18:14:02 -08:00
parent 7422e01a22
commit 28eb46d653
13 changed files with 340 additions and 285 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ export default function AdminPage() {
await handleSaveSettings(nextSettings);
} catch (error) {
console.error('Banner upload failed', error);
setBannerUploadError('Upload failed. Please try again.');
setBannerUploadError(error instanceof Error ? error.message : 'Upload failed. Please try again.');
} finally {
setIsUploadingBanner(false);
}
+2
View File
@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import { authenticateUser, createSession } from '@/lib/auth';
import { createStorageSession } from '@/lib/storage/session';
import { verifyTurnstileToken } from '@/lib/turnstile';
import { z } from 'zod';
@@ -30,6 +31,7 @@ export async function POST(request: Request) {
// Create session
await createSession(user.id);
await createStorageSession(user, data.password);
return NextResponse.json({
success: true,
+2
View File
@@ -1,8 +1,10 @@
import { NextResponse } from 'next/server';
import { destroySession } from '@/lib/auth';
import { clearStorageSession } from '@/lib/storage/session';
export async function POST() {
try {
await clearStorageSession();
await destroySession();
return NextResponse.json({ success: true });
+2
View File
@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import { registerUser, createSession } from '@/lib/auth';
import { createStorageSession } from '@/lib/storage/session';
import { db, nodes, users } from '@/db';
import { eq } from 'drizzle-orm';
import { verifyTurnstileToken } from '@/lib/turnstile';
@@ -97,6 +98,7 @@ 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,
+15 -18
View File
@@ -17,7 +17,8 @@ import {
BotHandleTakenError,
BotValidationError,
} from '@/lib/bots/botManager';
import { generateAndUploadAvatarToUserStorage, decryptS3Credentials } from '@/lib/storage/s3';
import { generateAndUploadAvatarToUserStorage } from '@/lib/storage/s3';
import { createStorageSession, getStorageSession } from '@/lib/storage/session';
// Schema for creating a bot
const createBotSchema = z.object({
@@ -43,7 +44,6 @@ const createBotSchema = z.object({
timezone: z.string().optional(),
}).optional(),
autonomousMode: z.boolean().optional(),
// Optional: password to generate avatar using owner's S3 storage
ownerPassword: z.string().optional(),
});
@@ -61,28 +61,25 @@ export async function POST(request: Request) {
const body = await request.json();
const data = createBotSchema.parse(body);
// Generate bot avatar using owner's S3 storage if password provided and no avatar URL
const storageSession =
(await getStorageSession(user.id)) ||
(data.ownerPassword ? await createStorageSession(user, data.ownerPassword) : null);
// Generate bot avatar using owner's S3 storage if no avatar URL
let botAvatarUrl: string | null | undefined = data.avatarUrl;
if (!botAvatarUrl && data.ownerPassword && user.storageAccessKeyEncrypted && user.storageSecretKeyEncrypted && user.storageBucket) {
if (!botAvatarUrl && storageSession) {
try {
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const botHandle = `${data.handle.toLowerCase()}@${nodeDomain}`;
// Decrypt the storage credentials
const { accessKeyId, secretAccessKey } = decryptS3Credentials(
user.storageAccessKeyEncrypted,
user.storageSecretKeyEncrypted,
data.ownerPassword
);
botAvatarUrl = await generateAndUploadAvatarToUserStorage(
botHandle,
user.storageEndpoint || undefined,
user.storagePublicBaseUrl || undefined,
user.storageRegion || 'auto',
user.storageBucket,
accessKeyId,
secretAccessKey
storageSession.endpoint || undefined,
storageSession.publicBaseUrl || undefined,
storageSession.region || 'us-east-1',
storageSession.bucket,
storageSession.accessKeyId,
storageSession.secretAccessKey
);
} catch (err) {
console.error('[Bot API] Failed to generate bot avatar:', err);
+17 -14
View File
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, media } from '@/db';
import { requireAuth } from '@/lib/auth';
import { uploadToUserStorage } from '@/lib/storage/s3';
import { getStorageSession, createStorageSession } from '@/lib/storage/session';
import { uploadWithStorageCredentials } from '@/lib/storage/s3';
import { v4 as uuidv4 } from 'uuid';
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB for images
@@ -47,10 +48,13 @@ export async function POST(req: NextRequest) {
}, { status: 400 });
}
// Require password to decrypt storage credentials
if (!password) {
return NextResponse.json({
error: 'Password required to upload media. Your storage credentials are encrypted and need your password to decrypt.'
const storageSession =
(await getStorageSession(user.id)) ||
(password ? await createStorageSession(user, password) : null);
if (!storageSession) {
return NextResponse.json({
error: 'Upload session expired. Please sign in again.'
}, { status: 401 });
}
@@ -58,18 +62,17 @@ export async function POST(req: NextRequest) {
const filename = `${uuidv4()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '')}`;
// Upload to user's own S3-compatible storage
const uploadResult = await uploadToUserStorage(
const uploadResult = await uploadWithStorageCredentials(
buffer,
filename,
file.type,
user.storageProvider as any,
user.storageEndpoint,
user.storagePublicBaseUrl,
user.storageRegion || 'us-east-1',
user.storageBucket || '',
user.storageAccessKeyEncrypted,
user.storageSecretKeyEncrypted,
password
storageSession.provider,
storageSession.endpoint,
storageSession.publicBaseUrl,
storageSession.region,
storageSession.bucket,
storageSession.accessKeyId,
storageSession.secretAccessKey
);
// Store media record with S3 URL
+44
View File
@@ -0,0 +1,44 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { requireAuth, verifyPassword } from '@/lib/auth';
import { clearStorageSession, createStorageSession } from '@/lib/storage/session';
const bodySchema = z.object({
password: z.string().min(1),
});
export async function POST(request: Request) {
try {
const user = await requireAuth();
const body = await request.json();
const data = bodySchema.parse(body);
if (!user.passwordHash) {
return NextResponse.json({ error: 'Account has no password set' }, { status: 400 });
}
const isValid = await verifyPassword(data.password, user.passwordHash);
if (!isValid) {
return NextResponse.json({ error: 'Incorrect password' }, { status: 401 });
}
await createStorageSession(user, data.password);
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid input', details: error.issues }, { status: 400 });
}
if (error instanceof Error && error.message === 'Authentication required') {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
console.error('Storage session error:', error);
return NextResponse.json({ error: 'Failed to refresh upload session' }, { status: 500 });
}
}
export async function DELETE() {
await clearStorageSession();
return NextResponse.json({ success: true });
}
-5
View File
@@ -47,7 +47,6 @@ export default function EditBotPage() {
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity');
const [storagePassword, setStoragePassword] = useState('');
const [formData, setFormData] = useState({
name: '',
@@ -371,8 +370,6 @@ export default function EditBotPage() {
setError('');
setFormData(prev => ({ ...prev, avatarUrl }));
}}
password={storagePassword}
onPasswordChange={setStoragePassword}
previewWidth={48}
previewHeight={48}
previewBorderRadius="50%"
@@ -387,8 +384,6 @@ export default function EditBotPage() {
setError('');
setFormData(prev => ({ ...prev, headerUrl }));
}}
password={storagePassword}
onPasswordChange={setStoragePassword}
previewWidth={120}
previewHeight={40}
previewBorderRadius="4px"
-6
View File
@@ -40,7 +40,6 @@ export default function NewBotPage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity');
const [storagePassword, setStoragePassword] = useState('');
const [formData, setFormData] = useState({
name: '',
@@ -183,7 +182,6 @@ export default function NewBotPage() {
llmProvider: formData.llmProvider,
llmModel: formData.llmModel,
llmApiKey: formData.llmApiKey,
ownerPassword: storagePassword || undefined,
autonomousMode: formData.autonomousMode,
schedule: formData.autonomousMode ? {
type: 'interval',
@@ -321,8 +319,6 @@ export default function NewBotPage() {
setError('');
setFormData(prev => ({ ...prev, avatarUrl }));
}}
password={storagePassword}
onPasswordChange={setStoragePassword}
previewWidth={48}
previewHeight={48}
previewBorderRadius="50%"
@@ -337,8 +333,6 @@ export default function NewBotPage() {
setError('');
setFormData(prev => ({ ...prev, headerUrl }));
}}
password={storagePassword}
onPasswordChange={setStoragePassword}
previewWidth={120}
previewHeight={40}
previewBorderRadius="4px"
-6
View File
@@ -115,8 +115,6 @@ export default function ProfilePage() {
const [isSaving, setIsSaving] = useState(false);
const [isBlocked, setIsBlocked] = useState(false);
const [showMenu, setShowMenu] = useState(false);
const [storagePassword, setStoragePassword] = useState('');
useEffect(() => {
setIsEditing(false);
setSaveError(null);
@@ -745,8 +743,6 @@ export default function ProfilePage() {
setSaveError(null);
setProfileForm({ ...profileForm, avatarUrl });
}}
password={storagePassword}
onPasswordChange={setStoragePassword}
previewWidth={48}
previewHeight={48}
previewBorderRadius="50%"
@@ -760,8 +756,6 @@ export default function ProfilePage() {
setSaveError(null);
setProfileForm({ ...profileForm, headerUrl });
}}
password={storagePassword}
onPasswordChange={setStoragePassword}
previewWidth={120}
previewHeight={40}
previewBorderRadius="4px"