Добавлена логика загрузки аватаров пользователей через API с использованием S3. Реализованы проверки на тип и размер файла, а также обработка ошибок. Обновлены компоненты UserSettings и useAuth для интеграции новой функциональности. Оптимизирована работа с кэшем Apollo Client для мгновенного обновления аватара в интерфейсе.
This commit is contained in:
@ -1,7 +1,119 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
||||||
|
|
||||||
|
const s3Client = new S3Client({
|
||||||
|
region: 'ru-1',
|
||||||
|
endpoint: 'https://s3.twcstorage.ru',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: 'I6XD2OR7YO2ZN6L6Z629',
|
||||||
|
secretAccessKey: '9xCOoafisG0aB9lJNvdLO1UuK73fBvMcpHMdijrJ'
|
||||||
|
},
|
||||||
|
forcePathStyle: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const BUCKET_NAME = '617774af-sfera'
|
||||||
|
|
||||||
|
// Разрешенные типы изображений для аватарки
|
||||||
|
const ALLOWED_IMAGE_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp'
|
||||||
|
]
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
return NextResponse.json({ message: 'Upload avatar API' })
|
try {
|
||||||
|
const formData = await request.formData()
|
||||||
|
const file = formData.get('file') as File
|
||||||
|
const userId = formData.get('userId') as string
|
||||||
|
|
||||||
|
if (!file || !userId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File and userId are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что файл не пустой
|
||||||
|
if (file.size === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File is empty' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем имя файла
|
||||||
|
if (!file.name || file.name.trim().length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid file name' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем тип файла
|
||||||
|
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `File type ${file.type} is not allowed. Only images are supported.` },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем размер файла (2MB для аватарки)
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File size must be less than 2MB' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем уникальное имя файла
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const fileExtension = file.name.split('.').pop()?.toLowerCase()
|
||||||
|
const fileName = `${userId}-${timestamp}.${fileExtension}`
|
||||||
|
const key = `avatars/${fileName}`
|
||||||
|
|
||||||
|
// Конвертируем файл в Buffer
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer())
|
||||||
|
|
||||||
|
// Очищаем метаданные от недопустимых символов
|
||||||
|
const cleanOriginalName = file.name.replace(/[^\w\s.-]/g, '_')
|
||||||
|
const cleanUserId = userId.replace(/[^\w-]/g, '')
|
||||||
|
|
||||||
|
// Загружаем в S3
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: file.type,
|
||||||
|
ACL: 'public-read',
|
||||||
|
Metadata: {
|
||||||
|
originalname: cleanOriginalName,
|
||||||
|
uploadedby: cleanUserId,
|
||||||
|
type: 'avatar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await s3Client.send(command)
|
||||||
|
|
||||||
|
// Возвращаем URL файла и метаданные
|
||||||
|
const url = `https://s3.twcstorage.ru/${BUCKET_NAME}/${key}`
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
url,
|
||||||
|
key,
|
||||||
|
originalName: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading avatar:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to upload avatar' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN } from '@/graphql/mutations'
|
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN } from '@/graphql/mutations'
|
||||||
|
import { GET_ME } from '@/graphql/queries'
|
||||||
|
import { apolloClient } from '@/lib/apollo-client'
|
||||||
import { formatPhone } from '@/lib/utils'
|
import { formatPhone } from '@/lib/utils'
|
||||||
import S3Service from '@/services/s3-service'
|
import S3Service from '@/services/s3-service'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
@ -35,12 +37,12 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Camera
|
Camera
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
export function UserSettings() {
|
export function UserSettings() {
|
||||||
const { getSidebarMargin } = useSidebar()
|
const { getSidebarMargin } = useSidebar()
|
||||||
const { user } = useAuth()
|
const { user, updateUser } = useAuth()
|
||||||
const [updateUserProfile, { loading: isSaving }] = useMutation(UPDATE_USER_PROFILE)
|
const [updateUserProfile, { loading: isSaving }] = useMutation(UPDATE_USER_PROFILE)
|
||||||
const [updateOrganizationByInn, { loading: isUpdatingOrganization }] = useMutation(UPDATE_ORGANIZATION_BY_INN)
|
const [updateOrganizationByInn, { loading: isUpdatingOrganization }] = useMutation(UPDATE_ORGANIZATION_BY_INN)
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
@ -48,6 +50,9 @@ export function UserSettings() {
|
|||||||
const [partnerLink, setPartnerLink] = useState('')
|
const [partnerLink, setPartnerLink] = useState('')
|
||||||
const [isGenerating, setIsGenerating] = useState(false)
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||||
|
const [localAvatarUrl, setLocalAvatarUrl] = useState<string | null>(null)
|
||||||
|
const phoneInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const whatsappInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
// Инициализируем данные из пользователя и организации
|
// Инициализируем данные из пользователя и организации
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@ -127,10 +132,10 @@ export function UserSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
orgPhone: orgPhone,
|
orgPhone: orgPhone || '+7',
|
||||||
managerName: user?.managerName || '',
|
managerName: user?.managerName || '',
|
||||||
telegram: customContacts?.telegram || '',
|
telegram: customContacts?.telegram || '',
|
||||||
whatsapp: customContacts?.whatsapp || '',
|
whatsapp: customContacts?.whatsapp || '+7',
|
||||||
email: email,
|
email: email,
|
||||||
orgName: org.name || '',
|
orgName: org.name || '',
|
||||||
address: org.address || '',
|
address: org.address || '',
|
||||||
@ -291,27 +296,73 @@ export function UserSettings() {
|
|||||||
try {
|
try {
|
||||||
const avatarUrl = await S3Service.uploadAvatar(file, user.id)
|
const avatarUrl = await S3Service.uploadAvatar(file, user.id)
|
||||||
|
|
||||||
|
// Сразу обновляем локальное состояние для мгновенного отображения
|
||||||
|
setLocalAvatarUrl(avatarUrl)
|
||||||
|
|
||||||
// Обновляем аватар пользователя через GraphQL
|
// Обновляем аватар пользователя через GraphQL
|
||||||
const result = await updateUserProfile({
|
const result = await updateUserProfile({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
avatar: avatarUrl
|
avatar: avatarUrl
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
update: (cache, { data }) => {
|
||||||
|
if (data?.updateUserProfile?.success) {
|
||||||
|
// Обновляем кеш Apollo Client
|
||||||
|
try {
|
||||||
|
const existingData: any = cache.readQuery({ query: GET_ME })
|
||||||
|
if (existingData?.me) {
|
||||||
|
cache.writeQuery({
|
||||||
|
query: GET_ME,
|
||||||
|
data: {
|
||||||
|
me: {
|
||||||
|
...existingData.me,
|
||||||
|
avatar: avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Cache update error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.data?.updateUserProfile?.success) {
|
if (result.data?.updateUserProfile?.success) {
|
||||||
setSaveMessage({ type: 'success', text: 'Аватар успешно обновлен! Обновляем страницу...' })
|
setSaveMessage({ type: 'success', text: 'Аватар успешно обновлен!' })
|
||||||
|
|
||||||
|
// Обновляем локальное состояние в useAuth для мгновенного отображения в сайдбаре
|
||||||
|
updateUser({ avatar: avatarUrl })
|
||||||
|
|
||||||
|
// Принудительно обновляем Apollo Client кеш
|
||||||
|
await apolloClient.refetchQueries({
|
||||||
|
include: [GET_ME]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Очищаем input файла
|
||||||
|
if (event.target) {
|
||||||
|
event.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем сообщение через 3 секунды
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload()
|
setSaveMessage(null)
|
||||||
}, 1000)
|
}, 3000)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to update avatar')
|
throw new Error(result.data?.updateUserProfile?.message || 'Failed to update avatar')
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading avatar:', error)
|
console.error('Error uploading avatar:', error)
|
||||||
setSaveMessage({ type: 'error', text: 'Ошибка при загрузке аватара' })
|
// Сбрасываем локальное состояние при ошибке
|
||||||
|
setLocalAvatarUrl(null)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Ошибка при загрузке аватара'
|
||||||
|
setSaveMessage({ type: 'error', text: errorMessage })
|
||||||
|
// Очищаем сообщение об ошибке через 5 секунд
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaveMessage(null)
|
||||||
|
}, 5000)
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingAvatar(false)
|
setIsUploadingAvatar(false)
|
||||||
}
|
}
|
||||||
@ -324,14 +375,69 @@ export function UserSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatPhoneInput = (value: string) => {
|
const formatPhoneInput = (value: string) => {
|
||||||
const cleaned = value.replace(/\D/g, '')
|
// Убираем все нецифровые символы
|
||||||
if (cleaned.length <= 1) return cleaned
|
const digitsOnly = value.replace(/\D/g, '')
|
||||||
|
|
||||||
|
// Если строка пустая, возвращаем +7
|
||||||
|
if (!digitsOnly) return '+7'
|
||||||
|
|
||||||
|
// Если пользователь ввел первую цифру не 7, добавляем 7 перед ней
|
||||||
|
let cleaned = digitsOnly
|
||||||
|
if (!cleaned.startsWith('7')) {
|
||||||
|
cleaned = '7' + cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем до 11 цифр (7 + 10 цифр номера)
|
||||||
|
cleaned = cleaned.slice(0, 11)
|
||||||
|
|
||||||
|
// Форматируем в зависимости от длины
|
||||||
|
if (cleaned.length <= 1) return '+7'
|
||||||
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
|
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
|
||||||
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
|
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
|
||||||
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
||||||
|
if (cleaned.length <= 11) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9)}`
|
||||||
|
|
||||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
|
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePhoneInputChange = (field: string, value: string, inputRef?: React.RefObject<HTMLInputElement>) => {
|
||||||
|
const currentInput = inputRef?.current
|
||||||
|
const currentCursorPosition = currentInput?.selectionStart || 0
|
||||||
|
const currentValue = formData[field as keyof typeof formData] as string || ''
|
||||||
|
|
||||||
|
// Если пользователь пытается удалить +7, предотвращаем это
|
||||||
|
if (value.length < 2) {
|
||||||
|
value = '+7'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatPhoneInput(value)
|
||||||
|
setFormData(prev => ({ ...prev, [field]: formatted }))
|
||||||
|
|
||||||
|
// Вычисляем новую позицию курсора
|
||||||
|
if (currentInput) {
|
||||||
|
setTimeout(() => {
|
||||||
|
let newCursorPosition = currentCursorPosition
|
||||||
|
|
||||||
|
// Если длина увеличилась (добавили цифру), передвигаем курсор
|
||||||
|
if (formatted.length > currentValue.length) {
|
||||||
|
newCursorPosition = currentCursorPosition + (formatted.length - currentValue.length)
|
||||||
|
}
|
||||||
|
// Если длина уменьшилась (удалили цифру), оставляем курсор на месте или сдвигаем немного
|
||||||
|
else if (formatted.length < currentValue.length) {
|
||||||
|
newCursorPosition = Math.min(currentCursorPosition, formatted.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не позволяем курсору находиться перед +7
|
||||||
|
newCursorPosition = Math.max(newCursorPosition, 2)
|
||||||
|
|
||||||
|
// Ограничиваем курсор длиной строки
|
||||||
|
newCursorPosition = Math.min(newCursorPosition, formatted.length)
|
||||||
|
|
||||||
|
currentInput.setSelectionRange(newCursorPosition, newCursorPosition)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatTelegram = (value: string) => {
|
const formatTelegram = (value: string) => {
|
||||||
// Убираем все символы кроме букв, цифр, _ и @
|
// Убираем все символы кроме букв, цифр, _ и @
|
||||||
let cleaned = value.replace(/[^a-zA-Z0-9_@]/g, '')
|
let cleaned = value.replace(/[^a-zA-Z0-9_@]/g, '')
|
||||||
@ -665,9 +771,9 @@ export function UserSettings() {
|
|||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-16 w-16">
|
<Avatar className="h-16 w-16">
|
||||||
{user?.avatar ? (
|
{(localAvatarUrl || user?.avatar) ? (
|
||||||
<Image
|
<Image
|
||||||
src={user.avatar}
|
src={localAvatarUrl || user.avatar}
|
||||||
alt="Аватар"
|
alt="Аватар"
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
@ -724,8 +830,16 @@ export function UserSettings() {
|
|||||||
<div>
|
<div>
|
||||||
<Label className="text-white/80 text-sm mb-2 block">Номер телефона организации</Label>
|
<Label className="text-white/80 text-sm mb-2 block">Номер телефона организации</Label>
|
||||||
<Input
|
<Input
|
||||||
|
ref={phoneInputRef}
|
||||||
value={formData.orgPhone || ''}
|
value={formData.orgPhone || ''}
|
||||||
onChange={(e) => handleInputChange('orgPhone', e.target.value)}
|
onChange={(e) => handlePhoneInputChange('orgPhone', e.target.value, phoneInputRef)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Предотвращаем удаление +7
|
||||||
|
if ((e.key === 'Backspace' || e.key === 'Delete') &&
|
||||||
|
phoneInputRef.current?.selectionStart <= 2) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="+7 (999) 999-99-99"
|
placeholder="+7 (999) 999-99-99"
|
||||||
readOnly={!isEditing}
|
readOnly={!isEditing}
|
||||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||||
@ -789,8 +903,16 @@ export function UserSettings() {
|
|||||||
WhatsApp
|
WhatsApp
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
ref={whatsappInputRef}
|
||||||
value={formData.whatsapp || ''}
|
value={formData.whatsapp || ''}
|
||||||
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
|
onChange={(e) => handlePhoneInputChange('whatsapp', e.target.value, whatsappInputRef)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Предотвращаем удаление +7
|
||||||
|
if ((e.key === 'Backspace' || e.key === 'Delete') &&
|
||||||
|
whatsappInputRef.current?.selectionStart <= 2) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="+7 (999) 999-99-99"
|
placeholder="+7 (999) 999-99-99"
|
||||||
readOnly={!isEditing}
|
readOnly={!isEditing}
|
||||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||||
|
@ -201,6 +201,8 @@ export const UPDATE_USER_PROFILE = gql`
|
|||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
phone
|
phone
|
||||||
|
avatar
|
||||||
|
managerName
|
||||||
organization {
|
organization {
|
||||||
id
|
id
|
||||||
inn
|
inn
|
||||||
|
@ -85,6 +85,7 @@ interface UseAuthReturn {
|
|||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
checkAuth: () => Promise<void>
|
checkAuth: () => Promise<void>
|
||||||
|
updateUser: (updatedUser: Partial<User>) => void
|
||||||
logout: () => void
|
logout: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +100,14 @@ export const useAuth = (): UseAuthReturn => {
|
|||||||
const [isCheckingAuth, setIsCheckingAuth] = useState(false) // Защита от повторных вызовов
|
const [isCheckingAuth, setIsCheckingAuth] = useState(false) // Защита от повторных вызовов
|
||||||
const { refreshApolloClient } = useApolloRefresh()
|
const { refreshApolloClient } = useApolloRefresh()
|
||||||
|
|
||||||
|
// Функция для обновления данных пользователя
|
||||||
|
const updateUser = (updatedUser: Partial<User>) => {
|
||||||
|
setUser(currentUser => {
|
||||||
|
if (!currentUser) return currentUser
|
||||||
|
return { ...currentUser, ...updatedUser }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const [sendSmsCodeMutation] = useMutation(SEND_SMS_CODE)
|
const [sendSmsCodeMutation] = useMutation(SEND_SMS_CODE)
|
||||||
const [verifySmsCodeMutation] = useMutation(VERIFY_SMS_CODE)
|
const [verifySmsCodeMutation] = useMutation(VERIFY_SMS_CODE)
|
||||||
const [registerFulfillmentMutation] = useMutation(REGISTER_FULFILLMENT_ORGANIZATION)
|
const [registerFulfillmentMutation] = useMutation(REGISTER_FULFILLMENT_ORGANIZATION)
|
||||||
@ -363,6 +372,7 @@ export const useAuth = (): UseAuthReturn => {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isLoading,
|
isLoading,
|
||||||
checkAuth,
|
checkAuth,
|
||||||
|
updateUser,
|
||||||
logout
|
logout
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,25 +27,21 @@ export class S3Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async uploadAvatar(file: File, userId: string): Promise<string> {
|
static async uploadAvatar(file: File, userId: string): Promise<string> {
|
||||||
const fileName = `${userId}-${Date.now()}.${file.name.split('.').pop()}`
|
|
||||||
const key = `avatars/${fileName}`
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Создаем FormData для загрузки
|
// Создаем FormData для загрузки
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
formData.append('key', key)
|
formData.append('userId', userId)
|
||||||
formData.append('bucket', s3Config.bucket)
|
|
||||||
|
|
||||||
// Пока используем простую загрузку через наш API
|
// Загружаем через наш API роут
|
||||||
// Позже можно будет сделать прямую загрузку в S3
|
|
||||||
const response = await fetch('/api/upload-avatar', {
|
const response = await fetch('/api/upload-avatar', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to upload avatar')
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.error || 'Failed to upload avatar')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
|
Reference in New Issue
Block a user