diff --git a/.env.example b/.env.example
index 6d5b4a8..31a4ae6 100644
--- a/.env.example
+++ b/.env.example
@@ -21,12 +21,3 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
# Optional: bot limits
# BOT_MAX_PER_USER=5
-
-# Optional: app-level storage fallback. Most media storage is user-owned and can be
-# configured in the app, so these are only needed if you want a shared default.
-# STORAGE_ENDPOINT=
-# STORAGE_REGION=
-# STORAGE_BUCKET=
-# STORAGE_ACCESS_KEY=
-# STORAGE_SECRET_KEY=
-# STORAGE_PUBLIC_BASE_URL=
diff --git a/docker-compose.yml b/docker-compose.yml
index 4bf5743..22441fe 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -70,14 +70,6 @@ services:
# Port configuration (auto or specific port)
PORT: ${PORT:-3000}
- # Shared S3 storage configuration
- STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-}
- STORAGE_REGION: ${STORAGE_REGION:-us-east-1}
- STORAGE_BUCKET: ${STORAGE_BUCKET:-}
- STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-}
- STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY:-}
- STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL:-}
-
# Node environment
NODE_ENV: production
volumes:
diff --git a/docker/.env.example b/docker/.env.example
index 2a401b2..2a02910 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -43,16 +43,3 @@ PORT=auto
# Maximum AI bots per user (default: 5)
# BOT_MAX_PER_USER=5
-
-# ===========================================
-# OPTIONAL: Shared S3 Storage
-# ===========================================
-# These are only needed if you want app-level shared storage defaults.
-# Most media storage is configured per user inside the app.
-
-# STORAGE_ENDPOINT=
-# STORAGE_REGION=us-east-1
-# STORAGE_BUCKET=
-# STORAGE_ACCESS_KEY=
-# STORAGE_SECRET_KEY=
-# STORAGE_PUBLIC_BASE_URL=
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 955c510..c17b3b3 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -47,6 +47,9 @@ RUN apk add --no-cache libc6-compat openssl netcat-openbsd wget
WORKDIR /app
+LABEL org.opencontainers.image.source="https://github.com/GnosysLabs/Synapsis"
+LABEL org.opencontainers.image.description="Synapsis self-hosted social node"
+
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
diff --git a/docker/README.md b/docker/README.md
index 5d99239..dfc6b20 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -42,15 +42,6 @@ Optional (advanced):
- `NEXT_PUBLIC_NODE_DOMAIN` to override the node domain (defaults to `DOMAIN`)
- `NEXT_PUBLIC_APP_URL` to override the public app URL used by background jobs (auto-derived from the node domain)
- `ALLOW_LOCALHOST=1` to allow `localhost` in production containers for local testing
-- Shared S3 storage env vars are available if you want app-level fallback storage
-
-Optional shared storage env vars:
-- `STORAGE_ENDPOINT`
-- `STORAGE_REGION`
-- `STORAGE_BUCKET`
-- `STORAGE_ACCESS_KEY`
-- `STORAGE_SECRET_KEY`
-- `STORAGE_PUBLIC_BASE_URL`
**Port Configuration:**
- `PORT=auto` (default) — Automatically finds an available port between 3000-3020
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 2e585b2..caf3e00 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -63,14 +63,6 @@ services:
# Admin emails
ADMIN_EMAILS: ${ADMIN_EMAILS}
- # S3 Storage configuration
- STORAGE_ENDPOINT: ${STORAGE_ENDPOINT}
- STORAGE_REGION: ${STORAGE_REGION:-us-east-1}
- STORAGE_BUCKET: ${STORAGE_BUCKET}
- STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
- STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
- STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL}
-
# Optional settings
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
diff --git a/docker/install.sh b/docker/install.sh
index 46453bf..9f33c73 100644
--- a/docker/install.sh
+++ b/docker/install.sh
@@ -122,7 +122,3 @@ echo "Next steps:"
echo " 1. Review ${INSTALL_DIR}/.env"
echo " 2. Start Synapsis:"
echo " cd ${INSTALL_DIR} && docker compose up -d"
-echo ""
-echo "One-line usage examples:"
-echo " curl -fsSL ${PUBLIC_INSTALL_URL} | bash"
-echo " curl -fsSL ${PUBLIC_INSTALL_URL} | DOMAIN=synapsis.example.com ADMIN_EMAILS=you@example.com bash"
diff --git a/src/app/api/media/upload/route.ts b/src/app/api/media/upload/route.ts
index c70193e..0ee2ec5 100644
--- a/src/app/api/media/upload/route.ts
+++ b/src/app/api/media/upload/route.ts
@@ -102,6 +102,11 @@ export async function POST(req: NextRequest) {
if (error instanceof Error && error.message === 'Authentication required') {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
+ if (error instanceof Error && error.message === 'Invalid storage password') {
+ return NextResponse.json({
+ error: 'Incorrect password. Please try again.'
+ }, { status: 401 });
+ }
if (error instanceof Error && error.message.includes('Storage')) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts
deleted file mode 100644
index bfd105f..0000000
--- a/src/app/api/uploads/route.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
-import { v4 as uuidv4 } from 'uuid';
-
-export async function POST(req: NextRequest) {
- try {
- const formData = await req.formData();
- const file = formData.get('file') as File;
-
- if (!file) {
- return NextResponse.json({ error: 'No file uploaded' }, { status: 400 });
- }
-
- const buffer = Buffer.from(await file.arrayBuffer());
- const filename = `${uuidv4()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '')}`;
-
- // S3 Configuration
- const s3 = new S3Client({
- region: process.env.STORAGE_REGION || 'us-east-1',
- endpoint: process.env.STORAGE_ENDPOINT,
- credentials: {
- accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
- secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
- },
- forcePathStyle: true, // Needed for many S3-compatible providers
- });
-
- const bucket = process.env.STORAGE_BUCKET || 'synapsis';
-
- await s3.send(new PutObjectCommand({
- Bucket: bucket,
- Key: filename,
- Body: buffer,
- ContentType: file.type,
- ACL: 'public-read', // Depends on bucket policy, but often needed
- }));
-
- // Construct Public URL
- let url = '';
- if (process.env.STORAGE_PUBLIC_BASE_URL) {
- url = `${process.env.STORAGE_PUBLIC_BASE_URL}/${filename}`;
- } else if (process.env.STORAGE_ENDPOINT) {
- url = `${process.env.STORAGE_ENDPOINT}/${bucket}/${filename}`;
- } else {
- return NextResponse.json({ error: 'Storage not configured - missing STORAGE_PUBLIC_BASE_URL or STORAGE_ENDPOINT' }, { status: 500 });
- }
-
- return NextResponse.json({ url });
- } catch (error) {
- console.error('Upload error:', error);
- return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
- }
-}
diff --git a/src/app/bots/[id]/edit/page.tsx b/src/app/bots/[id]/edit/page.tsx
index 0574079..3f4e44b 100644
--- a/src/app/bots/[id]/edit/page.tsx
+++ b/src/app/bots/[id]/edit/page.tsx
@@ -12,6 +12,7 @@ import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeftIcon } from '@/components/Icons';
+import { UserStorageImageUpload } from '@/components/UserStorageImageUpload';
import { Bot, Sparkles, Rss, Clock, Trash2 } from 'lucide-react';
interface ContentSource {
@@ -46,8 +47,7 @@ export default function EditBotPage() {
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity');
- const [uploadingAvatar, setUploadingAvatar] = useState(false);
- const [uploadingBanner, setUploadingBanner] = useState(false);
+ const [storagePassword, setStoragePassword] = useState('');
const [formData, setFormData] = useState({
name: '',
@@ -364,119 +364,37 @@ export default function EditBotPage() {
-
-
-
-
- {formData.avatarUrl && (
-
-

-
- )}
- {formData.avatarUrl && (
-
- )}
-
-
- Square image recommended (optional)
-
-
+ {
+ setError('');
+ setFormData(prev => ({ ...prev, avatarUrl }));
+ }}
+ password={storagePassword}
+ onPasswordChange={setStoragePassword}
+ previewWidth={48}
+ previewHeight={48}
+ previewBorderRadius="50%"
+ helperText="Square image recommended (optional)"
+ onError={setError}
+ />
-
-
-
-
- {formData.headerUrl && (
-
-

-
- )}
- {formData.headerUrl && (
-
- )}
-
-
- Wide image recommended, e.g. 1500x500 (optional)
-
-
+ {
+ setError('');
+ setFormData(prev => ({ ...prev, headerUrl }));
+ }}
+ password={storagePassword}
+ onPasswordChange={setStoragePassword}
+ previewWidth={120}
+ previewHeight={40}
+ previewBorderRadius="4px"
+ helperText="Wide image recommended, e.g. 1500x500 (optional)"
+ onError={setError}
+ />
);
diff --git a/src/app/bots/new/page.tsx b/src/app/bots/new/page.tsx
index 66a6d4f..498a44e 100644
--- a/src/app/bots/new/page.tsx
+++ b/src/app/bots/new/page.tsx
@@ -12,6 +12,7 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeftIcon } from '@/components/Icons';
+import { UserStorageImageUpload } from '@/components/UserStorageImageUpload';
import { Bot, Sparkles, Rss, Clock, Trash2 } from 'lucide-react';
interface ContentSource {
@@ -39,8 +40,7 @@ export default function NewBotPage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity');
- const [uploadingAvatar, setUploadingAvatar] = useState(false);
- const [uploadingBanner, setUploadingBanner] = useState(false);
+ const [storagePassword, setStoragePassword] = useState('');
const [formData, setFormData] = useState({
name: '',
@@ -183,6 +183,7 @@ export default function NewBotPage() {
llmProvider: formData.llmProvider,
llmModel: formData.llmModel,
llmApiKey: formData.llmApiKey,
+ ownerPassword: storagePassword || undefined,
autonomousMode: formData.autonomousMode,
schedule: formData.autonomousMode ? {
type: 'interval',
@@ -313,119 +314,37 @@ export default function NewBotPage() {
-
-
-
-
- {formData.avatarUrl && (
-
-

-
- )}
- {formData.avatarUrl && (
-
- )}
-
-
- Square image recommended (optional)
-
-
+ {
+ setError('');
+ setFormData(prev => ({ ...prev, avatarUrl }));
+ }}
+ password={storagePassword}
+ onPasswordChange={setStoragePassword}
+ previewWidth={48}
+ previewHeight={48}
+ previewBorderRadius="50%"
+ helperText="Square image recommended (optional)"
+ onError={setError}
+ />
-
-
-
-
- {formData.headerUrl && (
-
-

-
- )}
- {formData.headerUrl && (
-
- )}
-
-
- Wide image recommended, e.g. 1500x500 (optional)
-
-
+ {
+ setError('');
+ setFormData(prev => ({ ...prev, headerUrl }));
+ }}
+ password={storagePassword}
+ onPasswordChange={setStoragePassword}
+ previewWidth={120}
+ previewHeight={40}
+ previewBorderRadius="4px"
+ helperText="Wide image recommended, e.g. 1500x500 (optional)"
+ onError={setError}
+ />
);
diff --git a/src/app/u/[handle]/page.tsx b/src/app/u/[handle]/page.tsx
index e493fef..ff1094d 100644
--- a/src/app/u/[handle]/page.tsx
+++ b/src/app/u/[handle]/page.tsx
@@ -1,13 +1,14 @@
'use client';
-import { useState, useEffect, useRef, useCallback } from 'react';
+import { useState, useEffect, useCallback, useRef } from 'react';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons';
import { PostCard } from '@/components/PostCard';
import { User, Post } from '@/lib/types';
import AutoTextarea from '@/components/AutoTextarea';
-import { Rocket, MoreHorizontal, Mail, Camera } from 'lucide-react';
+import { UserStorageImageUpload } from '@/components/UserStorageImageUpload';
+import { Rocket, MoreHorizontal, Mail } from 'lucide-react';
import { useFormattedHandle } from '@/lib/utils/handle';
import { Bot } from 'lucide-react';
import { useAuth } from '@/lib/contexts/AuthContext';
@@ -114,68 +115,7 @@ export default function ProfilePage() {
const [isSaving, setIsSaving] = useState(false);
const [isBlocked, setIsBlocked] = useState(false);
const [showMenu, setShowMenu] = useState(false);
-
- const avatarInputRef = useRef(null);
- const headerInputRef = useRef(null);
-
- const handleUpload = async (e: React.ChangeEvent, field: 'avatarUrl' | 'headerUrl') => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- // Reset inputs so change event fires again for same file
- e.target.value = '';
-
- setIsSaving(true);
- try {
- // 1. Upload the file
- const formData = new FormData();
- formData.append('file', file);
- const uploadRes = await fetch('/api/uploads', {
- method: 'POST',
- body: formData,
- });
- const uploadData = await uploadRes.json();
-
- if (!uploadData.url) throw new Error('Upload failed');
- const imageUrl = uploadData.url;
-
- // 2. Update local form state immediately for UI feedback
- setProfileForm(prev => ({ ...prev, [field]: imageUrl }));
-
- // 3. Auto-save the profile change
- if (!isIdentityUnlocked) {
- throw new Error('Session expired. Please log in again.');
- }
-
- // Create partial update payload
- const updatePayload: any = { [field]: imageUrl };
-
- // Sign the action
- const signedPayload = await signUserAction('update_profile', updatePayload);
-
- const saveRes = await fetch('/api/auth/me', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(signedPayload),
- });
-
- const saveData = await saveRes.json();
-
- if (!saveRes.ok) {
- throw new Error(saveData.error || 'Failed to update profile');
- }
-
- // Update user state with the returned updated user
- setUser(saveData.user);
- // We do NOT exit edit mode automatically, allowing them to edit other fields
-
- } catch (err) {
- console.error(err);
- setSaveError(err instanceof Error ? err.message : 'Upload failed');
- } finally {
- setIsSaving(false);
- }
- };
+ const [storagePassword, setStoragePassword] = useState('');
useEffect(() => {
setIsEditing(false);
@@ -558,34 +498,8 @@ export default function ProfilePage() {
? `url(${isEditing ? profileForm.headerUrl : user.headerUrl}) center/cover`
: 'linear-gradient(135deg, var(--accent-muted) 0%, var(--background-tertiary) 100%)',
position: 'relative',
- cursor: isEditing ? 'pointer' : 'default',
- }}
- onClick={() => {
- if (isEditing) {
- headerInputRef.current?.click();
- }
}}
>
- {isEditing && (
-
-
- handleUpload(e, 'headerUrl')}
- style={{ display: 'none' }}
- />
-
- )}
{/* Avatar & Actions */}
@@ -604,12 +518,6 @@ export default function ProfilePage() {
border: '4px solid var(--background)',
marginTop: '-48px',
position: 'relative',
- cursor: isEditing ? 'pointer' : 'default',
- }}
- onClick={() => {
- if (isEditing) {
- avatarInputRef.current?.click();
- }
}}
>
{(isEditing ? profileForm.avatarUrl : user.avatarUrl) ? (
@@ -617,28 +525,6 @@ export default function ProfilePage() {
) : (
(user.displayName || user.handle).charAt(0).toUpperCase()
)}
-
- {isEditing && (
-
-
- handleUpload(e, 'avatarUrl')}
- style={{ display: 'none' }}
- />
-
- )}
@@ -852,7 +738,36 @@ export default function ProfilePage() {
maxLength={100}
/>
- {/* File inputs removed, now click-to-upload on visual elements */}
+ {
+ setSaveError(null);
+ setProfileForm({ ...profileForm, avatarUrl });
+ }}
+ password={storagePassword}
+ onPasswordChange={setStoragePassword}
+ previewWidth={48}
+ previewHeight={48}
+ previewBorderRadius="50%"
+ helperText="Square image recommended (optional)"
+ onError={(message) => setSaveError(message || null)}
+ />
+ {
+ setSaveError(null);
+ setProfileForm({ ...profileForm, headerUrl });
+ }}
+ password={storagePassword}
+ onPasswordChange={setStoragePassword}
+ previewWidth={120}
+ previewHeight={40}
+ previewBorderRadius="4px"
+ helperText="Wide image recommended, e.g. 1500x500 (optional)"
+ onError={(message) => setSaveError(message || null)}
+ />
{saveError && (
{saveError}
)}
diff --git a/src/components/UserStorageImageUpload.tsx b/src/components/UserStorageImageUpload.tsx
new file mode 100644
index 0000000..e2e3e6e
--- /dev/null
+++ b/src/components/UserStorageImageUpload.tsx
@@ -0,0 +1,272 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+
+interface UserStorageImageUploadProps {
+ label: string;
+ value: string;
+ onChange: (url: string) => void;
+ password: string;
+ onPasswordChange: (password: string) => void;
+ helperText?: string;
+ previewWidth?: number;
+ previewHeight?: number;
+ previewBorderRadius?: string;
+ onError?: (message: string) => void;
+}
+
+export function UserStorageImageUpload({
+ label,
+ value,
+ onChange,
+ password,
+ onPasswordChange,
+ helperText,
+ previewWidth = 48,
+ previewHeight = 48,
+ previewBorderRadius = '8px',
+ onError,
+}: UserStorageImageUploadProps) {
+ const inputRef = useRef(null);
+ const [isUploading, setIsUploading] = useState(false);
+ const [showPasswordPrompt, setShowPasswordPrompt] = useState(false);
+ const [pendingFile, setPendingFile] = useState(null);
+ const [passwordInput, setPasswordInput] = useState(password);
+ const [promptError, setPromptError] = useState('');
+
+ useEffect(() => {
+ if (!showPasswordPrompt) {
+ setPromptError('');
+ }
+ }, [showPasswordPrompt]);
+
+ useEffect(() => {
+ if (showPasswordPrompt && !passwordInput && password) {
+ setPasswordInput(password);
+ }
+ }, [showPasswordPrompt, password, passwordInput]);
+
+ const reportError = (message: string) => {
+ setPromptError(message);
+ onError?.(message);
+ };
+
+ const resetFileInput = () => {
+ if (inputRef.current) {
+ inputRef.current.value = '';
+ }
+ };
+
+ const uploadFile = async (file: File, uploadPassword: string) => {
+ setIsUploading(true);
+ setPromptError('');
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('password', uploadPassword);
+
+ const res = await fetch('/api/media/upload', {
+ method: 'POST',
+ body: formData,
+ });
+ const data = await res.json();
+
+ if (!res.ok || !data.url) {
+ const message = data.error || 'Upload failed';
+
+ if (res.status === 401) {
+ onPasswordChange('');
+ setPasswordInput('');
+ setPendingFile(file);
+ setShowPasswordPrompt(true);
+ setPromptError(message);
+ return;
+ }
+
+ throw new Error(message);
+ }
+
+ onPasswordChange(uploadPassword);
+ onChange(data.media?.url || data.url);
+ onError?.('');
+ setPendingFile(null);
+ setShowPasswordPrompt(false);
+ setPasswordInput(uploadPassword);
+ } catch (error) {
+ reportError(error instanceof Error ? error.message : 'Upload failed');
+ } finally {
+ setIsUploading(false);
+ resetFileInput();
+ }
+ };
+
+ const handleFileChange = async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ setPendingFile(file);
+
+ if (password.trim()) {
+ await uploadFile(file, password.trim());
+ return;
+ }
+
+ setPasswordInput('');
+ setShowPasswordPrompt(true);
+ };
+
+ const handlePasswordSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (!pendingFile) {
+ setShowPasswordPrompt(false);
+ return;
+ }
+
+ if (!passwordInput.trim()) {
+ setPromptError('Please enter your password');
+ return;
+ }
+
+ await uploadFile(pendingFile, passwordInput.trim());
+ };
+
+ const handleCancel = () => {
+ setShowPasswordPrompt(false);
+ setPendingFile(null);
+ setPasswordInput(password);
+ setPromptError('');
+ resetFileInput();
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {value && (
+
+

+
+ )}
+
+ {value && (
+
+ )}
+
+
+ {helperText && (
+
+ {helperText}
+
+ )}
+
+
+ {showPasswordPrompt && (
+
+
event.stopPropagation()}
+ >
+
+ Unlock storage upload
+
+
+ Enter your account password to upload this image to your own storage.
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/lib/auth/avatar.ts b/src/lib/auth/avatar.ts
deleted file mode 100644
index b193062..0000000
--- a/src/lib/auth/avatar.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
-import { v4 as uuidv4 } from 'uuid';
-
-export async function generateAndUploadAvatar(handle: string): Promise {
- try {
- // 1. Fetch the avatar from DiceBear (PNG format for better compatibility)
- const dicebearUrl = `https://api.dicebear.com/9.x/bottts-neutral/png?seed=${handle}`;
- const response = await fetch(dicebearUrl);
-
- if (!response.ok) {
- console.error(`Failed to fetch avatar from DiceBear: ${response.statusText}`);
- return null;
- }
-
- const arrayBuffer = await response.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
-
- // 2. Upload to S3
- const s3 = new S3Client({
- region: process.env.STORAGE_REGION || 'us-east-1',
- endpoint: process.env.STORAGE_ENDPOINT,
- credentials: {
- accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
- secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
- },
- forcePathStyle: true,
- });
-
- const bucket = process.env.STORAGE_BUCKET || 'synapsis';
- // Sanitize handle for filename just in case
- const safeHandle = handle.replace(/[^a-zA-Z0-9]/g, '');
- const filename = `${uuidv4()}-${safeHandle}-avatar.png`;
-
- await s3.send(new PutObjectCommand({
- Bucket: bucket,
- Key: filename,
- Body: buffer,
- ContentType: 'image/png',
- ACL: 'public-read',
- }));
-
- // 3. Construct Public URL
- let url = '';
- if (process.env.STORAGE_PUBLIC_BASE_URL) {
- url = `${process.env.STORAGE_PUBLIC_BASE_URL}/${filename}`;
- } else if (process.env.STORAGE_ENDPOINT) {
- // Basic fallback construction if base url is not set but endpoint is
- // This assumes path style access is okay if no custom domain
- const endpoint = process.env.STORAGE_ENDPOINT.replace(/\/+$/, '');
- url = `${endpoint}/${bucket}/${filename}`;
- } else {
- console.warn('Storage public URL not configured properly');
- return null;
- }
-
- return url;
-
- } catch (error) {
- console.error('Error generating/uploading avatar:', error);
- return null;
- }
-}
-
diff --git a/src/lib/storage/s3.ts b/src/lib/storage/s3.ts
index 85eb3b4..13328c7 100644
--- a/src/lib/storage/s3.ts
+++ b/src/lib/storage/s3.ts
@@ -30,15 +30,19 @@ export function decryptS3Credentials(
encryptedSecretKey: string,
password: string
): { accessKeyId: string; secretAccessKey: string } {
- const accessKeyId = decryptPrivateKey(
- deserializeEncryptedKey(encryptedAccessKey),
- password
- );
- const secretAccessKey = decryptPrivateKey(
- deserializeEncryptedKey(encryptedSecretKey),
- password
- );
- return { accessKeyId, secretAccessKey };
+ try {
+ const accessKeyId = decryptPrivateKey(
+ deserializeEncryptedKey(encryptedAccessKey),
+ password
+ );
+ const secretAccessKey = decryptPrivateKey(
+ deserializeEncryptedKey(encryptedSecretKey),
+ password
+ );
+ return { accessKeyId, secretAccessKey };
+ } catch {
+ throw new Error('Invalid storage password');
+ }
}
/**