new file: begushiybashkir/bbvue/src/components/AvatarUpload.vue

modified:   begushiybashkir/bbvue/src/stores/auth.js
	modified:   begushiybashkir/bbvue/src/views/Profile.vue
	modified:   begushiybashkir/bbvue/src/views/ProfileEdit.vue
	modified:   serv_nginx/api_bb/go.mod
	modified:   serv_nginx/api_bb/go.sum
	modified:   serv_nginx/api_bb/internal/handlers/auth.go
	new file:   serv_nginx/api_bb/internal/handlers/avatar.go
	modified:   serv_nginx/api_bb/internal/handlers/news_handler.go
	modified:   serv_nginx/api_bb/internal/handlers/user.go
	modified:   serv_nginx/api_bb/internal/models/user.go
	modified:   serv_nginx/api_bb/internal/repository/user_repository.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	modified:   serv_nginx/api_bb/internal/service/auth_service.go
	new file:   serv_nginx/api_bb/internal/service/avatar_service.go
	modified:   serv_nginx/api_bb/internal/service/news_service.go
	modified:   serv_nginx/api_bb/internal/service/user_service.go
	modified:   serv_nginx/api_bb/pkg/logger/interface.go
	new file:   serv_nginx/api_bb/pkg/logger/route_logger.go
add structure fix, page, path, routes, component, authStore
for upload, renew and delete avatar
This commit is contained in:
2025-10-13 00:51:13 +05:00
parent 6bb475acb2
commit e13545c5f1
19 changed files with 1269 additions and 580 deletions
@@ -0,0 +1,232 @@
<!-- components/AvatarUpload.vue -->
<template>
<div class="avatar-upload">
<div class="avatar-preview" @click="triggerFileInput">
<img v-if="previewUrl" :src="previewUrl" alt="Avatar" class="avatar-image">
<div v-else class="avatar-placeholder">
<span>📷</span>
<small>Добавить фото</small>
</div>
<div class="avatar-overlay">
<span></span>
</div>
</div>
<input
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/gif"
@change="handleFileSelect"
style="display: none;"
>
<div v-if="uploading" class="upload-progress">
Загрузка...
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="avatar-actions" v-if="showActions">
<button v-if="user?.avatar" class="btn btn-sm btn-danger" @click="deleteAvatar" :disabled="uploading">
🗑 Удалить
</button>
</div>
</div>
</template>
<script>
import { useAuthStore } from '../stores/auth'
export default {
name: 'AvatarUpload',
props: {
user: Object,
showActions: {
type: Boolean,
default: true
}
},
setup() {
const authStore = useAuthStore()
return { authStore }
},
data() {
return {
previewUrl: null,
uploading: false,
error: ''
}
},
watch: {
user: {
immediate: true,
handler(newUser) {
if (newUser?.avatar) {
this.previewUrl = this.getFullAvatarUrl(newUser.avatar)
} else {
this.previewUrl = null
}
}
}
},
methods: {
getFullAvatarUrl(avatarPath) {
if (!avatarPath) return null
if (avatarPath.startsWith('http')) return avatarPath
return `${import.meta.env.VITE_API_BASE_URL}${avatarPath}`
},
triggerFileInput() {
this.$refs.fileInput.click()
},
async handleFileSelect(event) {
const file = event.target.files[0]
if (!file) return
// Валидация
if (!file.type.startsWith('image/')) {
this.error = 'Пожалуйста, выберите файл изображения'
return
}
if (file.size > 10 * 1024 * 1024) { // 10MB
this.error = 'Размер файла не должен превышать 10MB'
return
}
// Превью
const reader = new FileReader()
reader.onload = (e) => {
this.previewUrl = e.target.result
}
reader.readAsDataURL(file)
// Загрузка
await this.uploadAvatar(file)
event.target.value = '' // Сброс input
},
async uploadAvatar(file) {
this.uploading = true
this.error = ''
try {
const result = await this.authStore.updateAvatar(file)
if (!result.success) {
this.error = result.error
this.previewUrl = this.getFullAvatarUrl(this.user?.avatar) // Восстанавливаем старый аватар
} else {
this.$emit('avatar-updated', result.avatar)
}
} catch (err) {
this.error = 'Ошибка загрузки: ' + err
this.previewUrl = this.getFullAvatarUrl(this.user?.avatar)
} finally {
this.uploading = false
}
},
async deleteAvatar() {
this.uploading = true
this.error = ''
try {
const result = await this.authStore.deleteAvatar()
if (!result.success) {
this.error = result.error
} else {
this.previewUrl = null
this.$emit('avatar-deleted')
}
} catch (err) {
this.error = 'Ошибка удаления: ' + err
} finally {
this.uploading = false
}
}
}
}
</script>
<style scoped>
.avatar-upload {
text-align: center;
}
.avatar-preview {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
position: relative;
cursor: pointer;
margin: 0 auto 1rem;
border: 3px solid #e0e0e0;
transition: border-color 0.3s;
}
.avatar-preview:hover {
border-color: #2e8b57;
}
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f5f5f5;
color: #666;
}
.avatar-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
color: white;
}
.avatar-preview:hover .avatar-overlay {
opacity: 1;
}
.avatar-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.upload-progress {
margin: 0.5rem 0;
color: #666;
font-size: 0.9rem;
}
</style>
+48 -4
View File
@@ -2,6 +2,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { apiClient, withLoading } from './helpers/api' import { apiClient, withLoading } from './helpers/api'
import { handleApiError } from './helpers/api';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
// State // State
@@ -13,7 +14,7 @@ export const useAuthStore = defineStore('auth', () => {
// Getters // Getters
const isAuthenticated = computed(() => !!token.value && !!user.value) const isAuthenticated = computed(() => !!token.value && !!user.value)
const userFullName = computed(() => const userFullName = computed(() =>
user.value ? `${user.value.firstName} ${user.value.lastName}` : '' user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
) )
@@ -37,7 +38,7 @@ export const useAuthStore = defineStore('auth', () => {
// Передаем store объект с loading и error // Передаем store объект с loading и error
return withLoading({ loading, error }, async () => { return withLoading({ loading, error }, async () => {
await apiClient.post('/auth/register', userData) await apiClient.post('/auth/register', userData)
// Auto-login after registration // Auto-login after registration
const loginResponse = await apiClient.post('/auth/login', { const loginResponse = await apiClient.post('/auth/login', {
email: userData.email, email: userData.email,
@@ -95,7 +96,7 @@ export const useAuthStore = defineStore('auth', () => {
const initializeAuth = async () => { const initializeAuth = async () => {
if (initialized.value || !token.value) return if (initialized.value || !token.value) return
initialized.value = true initialized.value = true
try { try {
@@ -107,6 +108,47 @@ export const useAuthStore = defineStore('auth', () => {
} }
} }
const updateAvatar = async (avatarFile) => {
const formData = new FormData()
formData.append('avatar', avatarFile)
try {
const response = await apiClient.post('/user/avatar/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.success) {
// Обновляем аватар в сторе
if (user.value) {
user.value.avatar = response.data.avatar
}
return { success: true, avatar: response.data.avatar }
}
} catch (error) {
const result = handleApiError(error)
return result
}
}
const deleteAvatar = async () => {
try {
const response = await apiClient.delete('/user/avatar/deleteg')
if (response.data.success) {
// Удаляем аватар из стора
if (user.value) {
user.value.avatar = null
}
return { success: true }
}
} catch (error) {
const result = handleApiError(error)
return result
}
}
return { return {
// State // State
user, user,
@@ -126,6 +168,8 @@ export const useAuthStore = defineStore('auth', () => {
fetchProfile, fetchProfile,
updateProfile, updateProfile,
initializeAuth, initializeAuth,
clearAuth clearAuth,
updateAvatar,
deleteAvatar
} }
}) })
+1 -1
View File
@@ -6,7 +6,7 @@
<div v-else-if="user" class="profile-content"> <div v-else-if="user" class="profile-content">
<div class="profile-header"> <div class="profile-header">
<img :src="getImageUrl('dinamo.jpg')" alt="Аватар" style="width: 100px; height: 100px; border-radius: 50%;"> <AvatarUpload :user="user" @avatar-updated="onAvatarUpdated" />
<h2>{{ user.firstName }} {{ user.lastName }}</h2> <h2>{{ user.firstName }} {{ user.lastName }}</h2>
<p>Участник с {{ joinDate }}</p> <p>Участник с {{ joinDate }}</p>
<p class="user-email">{{ user.email }}</p> <p class="user-email">{{ user.email }}</p>
@@ -2,6 +2,11 @@
<div class="page"> <div class="page">
<h1> Редактирование профиля</h1> <h1> Редактирование профиля</h1>
<div class="avatar-section">
<h3>Фотография профиля</h3>
<AvatarUpload :user="user" :show-actions="true" />
</div>
<div v-if="loading && !user" class="loading">Загрузка...</div> <div v-if="loading && !user" class="loading">Загрузка...</div>
<form v-else @submit.prevent="handleSubmit" class="profile-edit-form"> <form v-else @submit.prevent="handleSubmit" class="profile-edit-form">
@@ -90,9 +95,13 @@
<script> <script>
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import AvatarUpload from '../components/AvatarUpload.vue'
export default { export default {
name: 'ProfileEdit', name: 'ProfileEdit',
components: {
AvatarUpload
},
setup() { setup() {
const authStore = useAuthStore() const authStore = useAuthStore()
return { authStore } return { authStore }
+23
View File
@@ -12,15 +12,38 @@ require (
) )
require ( require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
) )
require ( require (
github.com/gin-gonic/gin v1.11.0
github.com/go-playground/validator/v10 v10.28.0 github.com/go-playground/validator/v10 v10.28.0
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+52
View File
@@ -1,8 +1,18 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
@@ -13,8 +23,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -29,27 +44,64 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+69 -71
View File
@@ -21,7 +21,7 @@ import (
type AuthHandler struct { type AuthHandler struct {
authService service.AuthService authService service.AuthService
jwtService service.JWTService jwtService service.JWTService
logger logger.Interface logger logger.LoggerInterface
} }
func NewAuthHandler(authService service.AuthService, jwtService service.JWTService) *AuthHandler { func NewAuthHandler(authService service.AuthService, jwtService service.JWTService) *AuthHandler {
@@ -165,87 +165,86 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
} }
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling login request", h.logger.Info("handling login request",
zap.String("method", r.Method), zap.String("method", r.Method),
zap.String("path", r.URL.Path), zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr), zap.String("remote_addr", r.RemoteAddr),
) )
// Проверяем Content-Type // Проверяем Content-Type
if r.Header.Get("Content-Type") != "application/json" { if r.Header.Get("Content-Type") != "application/json" {
h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type"))) h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type")))
utils.RespondWithError(w, http.StatusBadRequest, "Content-Type must be application/json") utils.RespondWithError(w, http.StatusBadRequest, "Content-Type must be application/json")
return return
} }
// Читаем и логируем тело запроса // Читаем и логируем тело запроса
bodyBytes, err := io.ReadAll(r.Body) bodyBytes, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
h.logger.Error("failed to read request body", zap.Error(err)) h.logger.Error("failed to read request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body") utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
return return
} }
defer r.Body.Close() defer r.Body.Close()
// Восстанавливаем тело // Восстанавливаем тело
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
h.logger.Debug("request body", zap.String("body", string(bodyBytes))) h.logger.Debug("request body", zap.String("body", string(bodyBytes)))
var req LoginRequest var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("JSON decode failed", h.logger.Error("JSON decode failed",
zap.Error(err), zap.Error(err),
zap.String("raw_body", string(bodyBytes)), zap.String("raw_body", string(bodyBytes)),
) )
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error()) utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return return
} }
req.Email = strings.TrimSpace(req.Email)
req.Email = strings.TrimSpace(req.Email) req.Password = strings.TrimSpace(req.Password)
req.Password = strings.TrimSpace(req.Password)
// Валидация // Валидация
if req.Email == "" || req.Password == "" { if req.Email == "" || req.Password == "" {
h.logger.Warn("validation failed", h.logger.Warn("validation failed",
zap.String("email", req.Email), zap.String("email", req.Email),
zap.Int("password_len", len(req.Password)), zap.Int("password_len", len(req.Password)),
) )
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required") utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
return return
} }
h.logger.Info("attempting login", zap.String("email", req.Email)) h.logger.Info("attempting login", zap.String("email", req.Email))
user, token, err := h.authService.Login(req.Email, req.Password) user, token, err := h.authService.Login(req.Email, req.Password)
if err != nil { if err != nil {
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err)) h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
utils.RespondWithError(w, http.StatusUnauthorized, err.Error()) utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
return return
} }
// Устанавливаем куки // Устанавливаем куки
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "auth_token", Name: "auth_token",
Value: token, Value: token,
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
Secure: false, Secure: false,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour), Expires: time.Now().Add(24 * time.Hour),
}) })
h.logger.Info("login successful", h.logger.Info("login successful",
zap.Uint("user_id", user.ID), zap.Uint("user_id", user.ID),
zap.String("email", user.Email), zap.String("email", user.Email),
) )
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Login successful", "message": "Login successful",
"token": token, "token": token,
"user": toUserResponse(user), "user": toUserResponse(user),
}) })
} }
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
@@ -277,4 +276,3 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
"message": "Logout successful", "message": "Logout successful",
}) })
} }
@@ -0,0 +1,97 @@
// handlers/avatar.go
package handlers
import (
"net/http"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type AvatarHandler struct {
logger logger.LoggerInterface
avatarService service.AvatarService
}
func NewAvatarHandler(avatarService service.AvatarService) *AvatarHandler {
return &AvatarHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "avatar"))),
avatarService: avatarService,
}
}
func (h *AvatarHandler) Routes() chi.Router {
r := chi.NewRouter()
r.Post("/upload", h.UploadAvatar)
r.Delete("/delete", h.DeleteAvatar)
return r
}
func (h *AvatarHandler) UploadAvatar(w http.ResponseWriter, r *http.Request) {
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Парсим multipart форму
if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
utils.RespondWithError(w, http.StatusBadRequest, "Failed to parse form: "+err.Error())
return
}
file, header, err := r.FormFile("avatar")
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Failed to get file: "+err.Error())
return
}
defer file.Close()
// Проверяем тип файла
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/jpg": true,
"image/png": true,
"image/gif": true,
}
if !allowedTypes[header.Header.Get("Content-Type")] {
utils.RespondWithError(w, http.StatusBadRequest, "Only JPEG, PNG and GIF images are allowed")
return
}
// Загружаем аватар
avatarPath, err := h.avatarService.UploadAvatar(user.ID, file, header)
if err != nil {
h.logger.Error("Failed to upload avatar", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to upload avatar: "+err.Error())
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Avatar uploaded successfully",
"avatar": avatarPath,
})
}
func (h *AvatarHandler) DeleteAvatar(w http.ResponseWriter, r *http.Request) {
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if err := h.avatarService.DeleteAvatar(user.ID); err != nil {
h.logger.Error("Failed to delete avatar", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete avatar: "+err.Error())
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Avatar deleted successfully",
})
}
@@ -14,265 +14,265 @@ import (
) )
type NewsHandler struct { type NewsHandler struct {
newsService service.NewsService newsService service.NewsService
logger logger.Interface logger logger.LoggerInterface
validator *validator.Validate validator *validator.Validate
} }
func NewNewsHandler(newsService service.NewsService, log logger.Interface) *NewsHandler { func NewNewsHandler(newsService service.NewsService, log logger.LoggerInterface) *NewsHandler {
return &NewsHandler{ return &NewsHandler{
newsService: newsService, newsService: newsService,
logger: log, logger: log,
validator: validator.New(), validator: validator.New(),
} }
} }
// GetNews возвращает список новостей с пагинацией и фильтрацией // GetNews возвращает список новостей с пагинацией и фильтрацией
func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
category := r.URL.Query().Get("category") category := r.URL.Query().Get("category")
if limit == 0 { if limit == 0 {
limit = 10 limit = 10
} }
if limit > 50 { if limit > 50 {
limit = 50 limit = 50
} }
news, total, err := h.newsService.GetAllNews(limit, offset, category) news, total, err := h.newsService.GetAllNews(limit, offset, category)
if err != nil { if err != nil {
h.logger.Error("Failed to get news", zap.Error(err)) h.logger.Error("Failed to get news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news, "news": news,
"total": total, "total": total,
"limit": limit, "limit": limit,
"offset": offset, "offset": offset,
}) })
} }
// GetNewsByID возвращает конкретную новость // GetNewsByID возвращает конкретную новость
func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID") utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return return
} }
news, err := h.newsService.GetNewsByID(uint(id)) news, err := h.newsService.GetNewsByID(uint(id))
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusNotFound, "News not found") utils.RespondWithError(w, http.StatusNotFound, "News not found")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, news) utils.RespondWithJSON(w, http.StatusOK, news)
} }
// CreateNews создает новую новость // CreateNews создает новую новость
func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint) userID, ok := r.Context().Value("userID").(uint)
if !ok { if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return return
} }
var req models.CreateNewsRequest var req models.CreateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil { if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body") utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return return
} }
if err := h.validator.Struct(req); err != nil { if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error()) utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return return
} }
news, err := h.newsService.CreateNews(req, userID) news, err := h.newsService.CreateNews(req, userID)
if err != nil { if err != nil {
h.logger.Error("Failed to create news", zap.Error(err)) h.logger.Error("Failed to create news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
return return
} }
utils.RespondWithJSON(w, http.StatusCreated, news) utils.RespondWithJSON(w, http.StatusCreated, news)
} }
// UpdateNews обновляет новость // UpdateNews обновляет новость
func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint) userID, ok := r.Context().Value("userID").(uint)
if !ok { if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return return
} }
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID") utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return return
} }
var req models.UpdateNewsRequest var req models.UpdateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil { if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body") utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return return
} }
if err := h.validator.Struct(req); err != nil { if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error()) utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return return
} }
news, err := h.newsService.UpdateNews(uint(id), req, userID) news, err := h.newsService.UpdateNews(uint(id), req, userID)
if err != nil { if err != nil {
if err.Error() == "access denied" { if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied") utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return return
} }
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, news) utils.RespondWithJSON(w, http.StatusOK, news)
} }
// DeleteNews удаляет новость // DeleteNews удаляет новость
func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint) userID, ok := r.Context().Value("userID").(uint)
if !ok { if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return return
} }
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID") utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return return
} }
err = h.newsService.DeleteNews(uint(id), userID) err = h.newsService.DeleteNews(uint(id), userID)
if err != nil { if err != nil {
if err.Error() == "access denied" { if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied") utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return return
} }
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"}) utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
} }
// CreateComment создает комментарий к новости // CreateComment создает комментарий к новости
func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint) userID, ok := r.Context().Value("userID").(uint)
if !ok { if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return return
} }
newsIDStr := chi.URLParam(r, "id") newsIDStr := chi.URLParam(r, "id")
newsID, err := strconv.ParseUint(newsIDStr, 10, 32) newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID") utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return return
} }
var req models.CreateCommentRequest var req models.CreateCommentRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil { if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body") utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return return
} }
if err := h.validator.Struct(req); err != nil { if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error()) utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return return
} }
comment, err := h.newsService.CreateComment(uint(newsID), req, userID) comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
return return
} }
utils.RespondWithJSON(w, http.StatusCreated, comment) utils.RespondWithJSON(w, http.StatusCreated, comment)
} }
// GetComments возвращает комментарии к новости // GetComments возвращает комментарии к новости
func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) {
newsIDStr := chi.URLParam(r, "id") newsIDStr := chi.URLParam(r, "id")
newsID, err := strconv.ParseUint(newsIDStr, 10, 32) newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID") utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return return
} }
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID)) comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, comments) utils.RespondWithJSON(w, http.StatusOK, comments)
} }
// DeleteComment удаляет комментарий // DeleteComment удаляет комментарий
func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint) userID, ok := r.Context().Value("userID").(uint)
if !ok { if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return return
} }
commentIDStr := chi.URLParam(r, "commentId") commentIDStr := chi.URLParam(r, "commentId")
commentID, err := strconv.ParseUint(commentIDStr, 10, 32) commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID") utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
return return
} }
err = h.newsService.DeleteComment(uint(commentID), userID) err = h.newsService.DeleteComment(uint(commentID), userID)
if err != nil { if err != nil {
if err.Error() == "access denied" { if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied") utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return return
} }
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"}) utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
} }
// GetUserNews возвращает новости конкретного пользователя // GetUserNews возвращает новости конкретного пользователя
func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) { func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint) userID, ok := r.Context().Value("userID").(uint)
if !ok { if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return return
} }
limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit == 0 { if limit == 0 {
limit = 10 limit = 10
} }
news, total, err := h.newsService.GetUserNews(userID, limit, offset) news, total, err := h.newsService.GetUserNews(userID, limit, offset)
if err != nil { if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news") utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
return return
} }
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news, "news": news,
"total": total, "total": total,
}) })
} }
+1 -1
View File
@@ -19,7 +19,7 @@ import (
) )
type UserHandler struct { type UserHandler struct {
logger logger.Interface logger logger.LoggerInterface
userService service.UserService userService service.UserService
} }
+16 -13
View File
@@ -8,26 +8,29 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// models/user.go - добавить поле Avatar
type User struct { type User struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"` Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"` Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"` FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"` LastName string `json:"last_name" gorm:"not null"`
Phone string `json:"phone"` Avatar string `json:"avatar"` // Путь к файлу аватара
Experience string `json:"experience"` Phone string `json:"phone"`
Goals string `json:"goals"` Experience string `json:"experience"`
Newsletter bool `json:"newsletter"` Goals string `json:"goals"`
Role string `json:"role" gorm:"default:user"` Newsletter bool `json:"newsletter"`
CreatedAt time.Time `json:"created_at"` Role string `json:"role" gorm:"default:user"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
} }
type UserUpdate struct { type UserUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Avatar string `json:"avatar"` // Добавить поле аватара
Phone string `json:"phone"` Phone string `json:"phone"`
Experience string `json:"experience"` Experience string `json:"experience"`
Goals string `json:"goals"` Goals string `json:"goals"`
@@ -8,66 +8,78 @@ import (
) )
type UserRepository interface { type UserRepository interface {
Create(user *models.User) error Create(user *models.User) error
FindByID(id uint) (*models.User, error) FindByID(id uint) (*models.User, error)
FindByEmail(email string) (*models.User, error) FindByEmail(email string) (*models.User, error)
Update(user *models.User) error Update(user *models.User) error
Delete(id uint) error Delete(id uint) error
UpdateExcludeEmail(userUpdate *models.User) error UpdateExcludeEmail(userUpdate *models.User) error
UpdateAvatar(userID uint, avatarPath string) error
}
func (r *userRepository) UpdateAvatar(userID uint, avatarPath string) error {
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("avatar", avatarPath)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
} }
type userRepository struct { type userRepository struct {
db *gorm.DB db *gorm.DB
} }
func NewUserRepository(db *gorm.DB) UserRepository { func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db} return &userRepository{db: db}
} }
func (r *userRepository) Create(user *models.User) error { func (r *userRepository) Create(user *models.User) error {
return r.db.Create(user).Error return r.db.Create(user).Error
} }
func (r *userRepository) FindByID(id uint) (*models.User, error) { func (r *userRepository) FindByID(id uint) (*models.User, error) {
var user models.User var user models.User
err := r.db.First(&user, id).Error err := r.db.First(&user, id).Error
return &user, err return &user, err
} }
func (r *userRepository) FindByEmail(email string) (*models.User, error) { func (r *userRepository) FindByEmail(email string) (*models.User, error) {
var user models.User var user models.User
err := r.db.Where("email = ?", email).First(&user).Error err := r.db.Where("email = ?", email).First(&user).Error
return &user, err return &user, err
} }
func (r *userRepository) Update(user *models.User) error { func (r *userRepository) Update(user *models.User) error {
return r.db.Save(user).Error return r.db.Save(user).Error
} }
func (r *userRepository) Delete(id uint) error { func (r *userRepository) Delete(id uint) error {
return r.db.Delete(&models.User{}, id).Error return r.db.Delete(&models.User{}, id).Error
} }
// repository/user_repository.go // repository/user_repository.go
func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error { func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error {
// Обновляем только разрешенные поля result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{ "first_name": userUpdate.FirstName,
"first_name": userUpdate.FirstName, "last_name": userUpdate.LastName,
"last_name": userUpdate.LastName, "avatar": userUpdate.Avatar, // Добавить обновление аватара
"phone": userUpdate.Phone, "phone": userUpdate.Phone,
"experience": userUpdate.Experience, "experience": userUpdate.Experience,
"goals": userUpdate.Goals, "goals": userUpdate.Goals,
"newsletter": userUpdate.Newsletter, "newsletter": userUpdate.Newsletter,
"updated_at": userUpdate.UpdatedAt, "updated_at": userUpdate.UpdatedAt,
}) })
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return fmt.Errorf("user not found") return fmt.Errorf("user not found")
} }
return nil return nil
} }
+10 -1
View File
@@ -11,7 +11,7 @@ import (
"api_bb/internal/handlers" "api_bb/internal/handlers"
"api_bb/internal/repository" "api_bb/internal/repository"
"api_bb/internal/service" "api_bb/internal/service"
"api_bb/pkg/logger" // Добавьте импорт логгера "api_bb/pkg/logger"
"api_bb/pkg/middleware" "api_bb/pkg/middleware"
) )
@@ -36,12 +36,14 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер
userService := service.NewUserService(userRepo, jwtService, baseLogger) userService := service.NewUserService(userRepo, jwtService, baseLogger)
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger) newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
avatarService := service.NewAvatarService(userRepo, baseLogger)
// Initialize handlers // Initialize handlers
healthHandler := handlers.NewHealthHandler() healthHandler := handlers.NewHealthHandler()
authHandler := handlers.NewAuthHandler(authService, jwtService) authHandler := handlers.NewAuthHandler(authService, jwtService)
userHandler := handlers.NewUserHandler(&userService) userHandler := handlers.NewUserHandler(&userService)
newsHandler := handlers.NewNewsHandler(newsService, baseLogger) newsHandler := handlers.NewNewsHandler(newsService, baseLogger)
avatarHandler := handlers.NewAvatarHandler(avatarService)
// Health routes // Health routes
r.Mount("/api", healthHandler.Routes()) r.Mount("/api", healthHandler.Routes())
@@ -59,6 +61,9 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
r.Use(middleware.RequireAuth) r.Use(middleware.RequireAuth)
r.Mount("/", userHandler.Routes()) r.Mount("/", userHandler.Routes())
r.Mount("/avatar", avatarHandler.Routes())
// Здесь будут другие защищенные маршруты пользователя // Здесь будут другие защищенные маршруты пользователя
}) })
@@ -90,5 +95,9 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
// r.Mount("/reviews", reviewHandler.Routes()) // r.Mount("/reviews", reviewHandler.Routes())
}) })
// Логируем все зарегистрированные маршруты
routeLogger := logger.NewRouteLogger(baseLogger)
routeLogger.LogRoutes(r)
return r return r
} }
@@ -20,10 +20,10 @@ type AuthService interface {
type authService struct { type authService struct {
userRepo repository.UserRepository userRepo repository.UserRepository
jwtService JWTService jwtService JWTService
logger logger.Interface logger logger.LoggerInterface
} }
func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, log logger.Interface) AuthService { func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) AuthService {
// Создаем логгер с контекстом для сервиса // Создаем логгер с контекстом для сервиса
serviceLogger := log.With(zap.String("service", "auth")) serviceLogger := log.With(zap.String("service", "auth"))
@@ -34,7 +34,6 @@ func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, l
} }
} }
func (s *authService) Register(user *models.User) error { func (s *authService) Register(user *models.User) error {
s.logger.Info("Registering new user", s.logger.Info("Registering new user",
zap.String("email", user.Email), zap.String("email", user.Email),
@@ -0,0 +1,113 @@
// service/avatar_service.go
package service
import (
"api_bb/internal/repository"
"api_bb/pkg/logger"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
)
type AvatarService interface {
UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error)
DeleteAvatar(userID uint) error
GetAvatarPath(userID uint) (string, error)
}
type avatarService struct {
userRepo repository.UserRepository
logger logger.LoggerInterface
}
func NewAvatarService(userRepo repository.UserRepository, log logger.LoggerInterface) AvatarService {
return &avatarService{
userRepo: userRepo,
logger: log.With(zap.String("service", "avatar")),
}
}
func (s *avatarService) UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error) {
// Проверяем пользователя
user, err := s.userRepo.FindByID(userID)
if err != nil {
return "", fmt.Errorf("user not found")
}
// Создаем директорию для аватаров если не существует
uploadDir := "./uploads/avatars"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
return "", fmt.Errorf("failed to create upload directory: %v", err)
}
// Генерируем уникальное имя файла
fileExt := filepath.Ext(header.Filename)
fileName := fmt.Sprintf("avatar_%d_%d%s", userID, time.Now().Unix(), fileExt)
filePath := filepath.Join(uploadDir, fileName)
// Создаем файл
dst, err := os.Create(filePath)
if err != nil {
return "", fmt.Errorf("failed to create file: %v", err)
}
defer dst.Close()
// Копируем содержимое
if _, err := io.Copy(dst, file); err != nil {
return "", fmt.Errorf("failed to save file: %v", err)
}
// Удаляем старый аватар если существует
if user.Avatar != "" {
oldPath := strings.TrimPrefix(user.Avatar, "/")
if _, err := os.Stat(oldPath); err == nil {
os.Remove(oldPath)
}
}
// Сохраняем путь в БД
avatarPath := "/uploads/avatars/" + fileName
if err := s.userRepo.UpdateAvatar(userID, avatarPath); err != nil {
// Если не удалось сохранить в БД, удаляем загруженный файл
os.Remove(filePath)
return "", fmt.Errorf("failed to update avatar in database: %v", err)
}
return avatarPath, nil
}
func (s *avatarService) DeleteAvatar(userID uint) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return fmt.Errorf("user not found")
}
if user.Avatar == "" {
return nil // Аватара нет, ничего не делаем
}
// Удаляем файл
filePath := strings.TrimPrefix(user.Avatar, "/")
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err != nil {
s.logger.Warn("Failed to delete avatar file", zap.Error(err))
}
}
// Очищаем поле в БД
return s.userRepo.UpdateAvatar(userID, "")
}
func (s *avatarService) GetAvatarPath(userID uint) (string, error) {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return "", err
}
return user.Avatar, nil
}
+189 -190
View File
@@ -10,237 +10,236 @@ import (
) )
type NewsService interface { type NewsService interface {
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
GetNewsByID(id uint) (*models.NewsResponse, error) GetNewsByID(id uint) (*models.NewsResponse, error)
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
DeleteNews(id uint, userID uint) error DeleteNews(id uint, userID uint) error
IncrementViews(id uint) error IncrementViews(id uint) error
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
DeleteComment(commentID, userID uint) error DeleteComment(commentID, userID uint) error
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
} }
type newsService struct { type newsService struct {
newsRepo repository.NewsRepository newsRepo repository.NewsRepository
commentRepo repository.CommentRepository commentRepo repository.CommentRepository
logger logger.Interface logger logger.LoggerInterface
} }
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.Interface) NewsService { func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.LoggerInterface) NewsService {
serviceLogger := log.With(zap.String("service", "news")) serviceLogger := log.With(zap.String("service", "news"))
return &newsService{ return &newsService{
newsRepo: newsRepo, newsRepo: newsRepo,
commentRepo: commentRepo, commentRepo: commentRepo,
logger: serviceLogger, logger: serviceLogger,
} }
} }
func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) { func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) {
news := &models.News{ news := &models.News{
Title: req.Title, Title: req.Title,
Excerpt: req.Excerpt, Excerpt: req.Excerpt,
Content: req.Content, Content: req.Content,
Image: req.Image, Image: req.Image,
Category: req.Category, Category: req.Category,
AuthorID: authorID, AuthorID: authorID,
} }
if err := s.newsRepo.Create(news); err != nil { if err := s.newsRepo.Create(news); err != nil {
s.logger.Error("Failed to create news", zap.Error(err)) s.logger.Error("Failed to create news", zap.Error(err))
return nil, errors.New("failed to create news") return nil, errors.New("failed to create news")
} }
// Получаем созданную новость с автором // Получаем созданную новость с автором
createdNews, err := s.newsRepo.GetByID(news.ID) createdNews, err := s.newsRepo.GetByID(news.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.toNewsResponse(createdNews), nil return s.toNewsResponse(createdNews), nil
} }
func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) { func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) {
news, err := s.newsRepo.GetByID(id) news, err := s.newsRepo.GetByID(id)
if err != nil { if err != nil {
return nil, errors.New("news not found") return nil, errors.New("news not found")
} }
// Увеличиваем счетчик просмотров // Увеличиваем счетчик просмотров
go s.newsRepo.IncrementViews(id) go s.newsRepo.IncrementViews(id)
return s.toNewsResponse(news), nil return s.toNewsResponse(news), nil
} }
func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) { func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) {
news, total, err := s.newsRepo.GetAll(limit, offset, category) news, total, err := s.newsRepo.GetAll(limit, offset, category)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
responses := make([]models.NewsResponse, len(news)) responses := make([]models.NewsResponse, len(news))
for i, n := range news { for i, n := range news {
responses[i] = *s.toNewsResponse(&n) responses[i] = *s.toNewsResponse(&n)
} }
return responses, total, nil return responses, total, nil
} }
func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) { func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) {
news, err := s.newsRepo.GetByID(id) news, err := s.newsRepo.GetByID(id)
if err != nil { if err != nil {
return nil, errors.New("news not found") return nil, errors.New("news not found")
} }
// Проверяем права доступа // Проверяем права доступа
if news.AuthorID != userID { if news.AuthorID != userID {
return nil, errors.New("access denied") return nil, errors.New("access denied")
} }
// Обновляем поля // Обновляем поля
if req.Title != "" { if req.Title != "" {
news.Title = req.Title news.Title = req.Title
} }
if req.Excerpt != "" { if req.Excerpt != "" {
news.Excerpt = req.Excerpt news.Excerpt = req.Excerpt
} }
if req.Content != "" { if req.Content != "" {
news.Content = req.Content news.Content = req.Content
} }
if req.Image != "" { if req.Image != "" {
news.Image = req.Image news.Image = req.Image
} }
if req.Category != "" { if req.Category != "" {
news.Category = req.Category news.Category = req.Category
} }
if err := s.newsRepo.Update(news); err != nil { if err := s.newsRepo.Update(news); err != nil {
return nil, err return nil, err
} }
return s.toNewsResponse(news), nil return s.toNewsResponse(news), nil
} }
func (s *newsService) DeleteNews(id uint, userID uint) error { func (s *newsService) DeleteNews(id uint, userID uint) error {
news, err := s.newsRepo.GetByID(id) news, err := s.newsRepo.GetByID(id)
if err != nil { if err != nil {
return errors.New("news not found") return errors.New("news not found")
} }
// Проверяем права доступа // Проверяем права доступа
if news.AuthorID != userID { if news.AuthorID != userID {
return errors.New("access denied") return errors.New("access denied")
} }
return s.newsRepo.Delete(id) return s.newsRepo.Delete(id)
} }
func (s *newsService) IncrementViews(id uint) error { func (s *newsService) IncrementViews(id uint) error {
return s.newsRepo.IncrementViews(id) return s.newsRepo.IncrementViews(id)
} }
func (s *newsService) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) { func (s *newsService) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) {
// Проверяем существование новости // Проверяем существование новости
_, err := s.newsRepo.GetByID(newsID) _, err := s.newsRepo.GetByID(newsID)
if err != nil { if err != nil {
return nil, errors.New("news not found") return nil, errors.New("news not found")
} }
comment := &models.Comment{ comment := &models.Comment{
Content: req.Content, Content: req.Content,
NewsID: newsID, NewsID: newsID,
AuthorID: authorID, AuthorID: authorID,
} }
if err := s.commentRepo.Create(comment); err != nil { if err := s.commentRepo.Create(comment); err != nil {
return nil, err return nil, err
} }
// Получаем созданный комментарий с автором // Получаем созданный комментарий с автором
createdComment, err := s.commentRepo.GetByID(comment.ID) createdComment, err := s.commentRepo.GetByID(comment.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.toCommentResponse(createdComment), nil return s.toCommentResponse(createdComment), nil
} }
func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) { func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) {
comments, err := s.commentRepo.GetByNewsID(newsID) comments, err := s.commentRepo.GetByNewsID(newsID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
responses := make([]models.CommentResponse, len(comments)) responses := make([]models.CommentResponse, len(comments))
for i, c := range comments { for i, c := range comments {
responses[i] = *s.toCommentResponse(&c) responses[i] = *s.toCommentResponse(&c)
} }
return responses, nil return responses, nil
} }
func (s *newsService) DeleteComment(commentID, userID uint) error { func (s *newsService) DeleteComment(commentID, userID uint) error {
comment, err := s.commentRepo.GetByID(commentID) comment, err := s.commentRepo.GetByID(commentID)
if err != nil { if err != nil {
return errors.New("comment not found") return errors.New("comment not found")
} }
// Проверяем права доступа // Проверяем права доступа
if comment.AuthorID != userID { if comment.AuthorID != userID {
return errors.New("access denied") return errors.New("access denied")
} }
return s.commentRepo.Delete(commentID) return s.commentRepo.Delete(commentID)
} }
func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) { func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) {
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset) news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
responses := make([]models.NewsResponse, len(news)) responses := make([]models.NewsResponse, len(news))
for i, n := range news { for i, n := range news {
responses[i] = *s.toNewsResponse(&n) responses[i] = *s.toNewsResponse(&n)
} }
return responses, total, nil return responses, total, nil
} }
// Вспомогательные методы для преобразования // Вспомогательные методы для преобразования
func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse { func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse {
return &models.NewsResponse{ return &models.NewsResponse{
ID: news.ID, ID: news.ID,
CreatedAt: news.CreatedAt, CreatedAt: news.CreatedAt,
UpdatedAt: news.UpdatedAt, UpdatedAt: news.UpdatedAt,
Title: news.Title, Title: news.Title,
Excerpt: news.Excerpt, Excerpt: news.Excerpt,
Content: news.Content, Content: news.Content,
Image: news.Image, Image: news.Image,
Category: news.Category, Category: news.Category,
Views: news.Views, Views: news.Views,
Author: models.AuthorInfo{ Author: models.AuthorInfo{
ID: news.Author.ID, ID: news.Author.ID,
FirstName: news.Author.FirstName, FirstName: news.Author.FirstName,
LastName: news.Author.LastName, LastName: news.Author.LastName,
}, },
Comments: len(news.Comments), Comments: len(news.Comments),
} }
} }
func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse { func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse {
return &models.CommentResponse{ return &models.CommentResponse{
ID: comment.ID, ID: comment.ID,
CreatedAt: comment.CreatedAt, CreatedAt: comment.CreatedAt,
Content: comment.Content, Content: comment.Content,
Author: models.AuthorInfo{ Author: models.AuthorInfo{
ID: comment.Author.ID, ID: comment.Author.ID,
FirstName: comment.Author.FirstName, FirstName: comment.Author.FirstName,
LastName: comment.Author.LastName, LastName: comment.Author.LastName,
}, },
} }
} }
@@ -18,16 +18,15 @@ type UserService interface {
type userService struct { type userService struct {
userRepo repository.UserRepository userRepo repository.UserRepository
jwtService JWTService jwtService JWTService
logger logger.Interface logger logger.LoggerInterface
} }
// UpdateProfile implements UserService. // UpdateProfile implements UserService.
func (s userService) UpdateProfile(user *models.User) error { func (s userService) UpdateProfile(user *models.User) error {
panic("unimplemented") panic("unimplemented")
} }
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.Interface) userService { func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) userService {
// Создаем логгер с контекстом для сервиса // Создаем логгер с контекстом для сервиса
serviceLogger := log.With(zap.String("service", "user")) serviceLogger := log.With(zap.String("service", "user"))
+31 -31
View File
@@ -3,73 +3,73 @@ package logger
import "go.uber.org/zap" import "go.uber.org/zap"
// Interface определяет контракт для логгера // LoggerInterface определяет контракт для логгера
type Interface interface { type LoggerInterface interface {
Debug(msg string, fields ...zap.Field) Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field) Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field) Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field) Error(msg string, fields ...zap.Field)
Fatal(msg string, fields ...zap.Field) Fatal(msg string, fields ...zap.Field)
Debugf(template string, args ...interface{}) Debugf(template string, args ...interface{})
Infof(template string, args ...interface{}) Infof(template string, args ...interface{})
Warnf(template string, args ...interface{}) Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{}) Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{}) Fatalf(template string, args ...interface{})
With(fields ...zap.Field) Interface With(fields ...zap.Field) LoggerInterface
} }
// wrapper обертка для zap.Logger // wrapper обертка для zap.Logger
type wrapper struct { type wrapper struct {
logger *zap.Logger logger *zap.Logger
} }
// NewWrapper создает новую обертку // NewWrapper создает новую обертку
func NewWrapper(logger *zap.Logger) Interface { func NewWrapper(logger *zap.Logger) LoggerInterface {
return &wrapper{logger: logger} return &wrapper{logger: logger}
} }
func (w *wrapper) Debug(msg string, fields ...zap.Field) { func (w *wrapper) Debug(msg string, fields ...zap.Field) {
w.logger.Debug(msg, fields...) w.logger.Debug(msg, fields...)
} }
func (w *wrapper) Info(msg string, fields ...zap.Field) { func (w *wrapper) Info(msg string, fields ...zap.Field) {
w.logger.Info(msg, fields...) w.logger.Info(msg, fields...)
} }
func (w *wrapper) Warn(msg string, fields ...zap.Field) { func (w *wrapper) Warn(msg string, fields ...zap.Field) {
w.logger.Warn(msg, fields...) w.logger.Warn(msg, fields...)
} }
func (w *wrapper) Error(msg string, fields ...zap.Field) { func (w *wrapper) Error(msg string, fields ...zap.Field) {
w.logger.Error(msg, fields...) w.logger.Error(msg, fields...)
} }
func (w *wrapper) Fatal(msg string, fields ...zap.Field) { func (w *wrapper) Fatal(msg string, fields ...zap.Field) {
w.logger.Fatal(msg, fields...) w.logger.Fatal(msg, fields...)
} }
func (w *wrapper) Debugf(template string, args ...interface{}) { func (w *wrapper) Debugf(template string, args ...interface{}) {
w.logger.Sugar().Debugf(template, args...) w.logger.Sugar().Debugf(template, args...)
} }
func (w *wrapper) Infof(template string, args ...interface{}) { func (w *wrapper) Infof(template string, args ...interface{}) {
w.logger.Sugar().Infof(template, args...) w.logger.Sugar().Infof(template, args...)
} }
func (w *wrapper) Warnf(template string, args ...interface{}) { func (w *wrapper) Warnf(template string, args ...interface{}) {
w.logger.Sugar().Warnf(template, args...) w.logger.Sugar().Warnf(template, args...)
} }
func (w *wrapper) Errorf(template string, args ...interface{}) { func (w *wrapper) Errorf(template string, args ...interface{}) {
w.logger.Sugar().Errorf(template, args...) w.logger.Sugar().Errorf(template, args...)
} }
func (w *wrapper) Fatalf(template string, args ...interface{}) { func (w *wrapper) Fatalf(template string, args ...interface{}) {
w.logger.Sugar().Fatalf(template, args...) w.logger.Sugar().Fatalf(template, args...)
} }
func (w *wrapper) With(fields ...zap.Field) Interface { func (w *wrapper) With(fields ...zap.Field) LoggerInterface {
return &wrapper{logger: w.logger.With(fields...)} return &wrapper{logger: w.logger.With(fields...)}
} }
@@ -0,0 +1,100 @@
package logger
import (
"net/http"
"sort"
"strings"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type RouteLogger struct {
logger LoggerInterface
}
func NewRouteLogger(log LoggerInterface) *RouteLogger {
return &RouteLogger{
logger: log,
}
}
func (rl *RouteLogger) LogRoutes(router *chi.Mux) {
routes := rl.extractRoutes(router)
rl.printFormattedRoutes(routes)
}
func (rl *RouteLogger) extractRoutes(router *chi.Mux) []RouteInfo {
var routes []RouteInfo
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
if route != "" {
routes = append(routes, RouteInfo{
Method: method,
Path: route,
})
}
return nil
}
if err := chi.Walk(router, walkFunc); err != nil {
rl.logger.Error("Failed to walk routes", zap.Error(err))
}
return routes
}
func (rl *RouteLogger) printFormattedRoutes(routes []RouteInfo) {
if len(routes) == 0 {
rl.logger.Info("No routes found")
return
}
// Группируем по пути
routesByPath := make(map[string][]string)
for _, route := range routes {
routesByPath[route.Path] = append(routesByPath[route.Path], route.Method)
}
// Сортируем пути
var paths []string
for path := range routesByPath {
paths = append(paths, path)
}
sort.Strings(paths)
rl.logger.Info("📋 Registered API Routes:")
rl.logger.Info("┌──────────────────────────────────────────────────────────────┐")
for _, path := range paths {
methods := routesByPath[path]
sort.Strings(methods)
methodsStr := strings.Join(methods, ", ")
if len(methodsStr) > 12 {
methodsStr = methodsStr[:9] + "..."
}
methodField := methodsStr
if len(methodField) < 12 {
methodField = methodField + strings.Repeat(" ", 12-len(methodField))
}
pathField := path
if len(pathField) > 45 {
pathField = pathField[:42] + "..."
} else {
pathField = pathField + strings.Repeat(" ", 45-len(pathField))
}
rl.logger.Info("│ " + methodField + " " + pathField + " │")
}
rl.logger.Info("└──────────────────────────────────────────────────────────────┘")
rl.logger.Info("Total routes registered: %d", zap.Int("count", len(routes)))
}
type RouteInfo struct {
Method string
Path string
}