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 { ref, computed } from 'vue'
import { apiClient, withLoading } from './helpers/api'
import { handleApiError } from './helpers/api';
export const useAuthStore = defineStore('auth', () => {
// State
@@ -13,7 +14,7 @@ export const useAuthStore = defineStore('auth', () => {
// Getters
const isAuthenticated = computed(() => !!token.value && !!user.value)
const userFullName = computed(() =>
const userFullName = computed(() =>
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
)
@@ -37,7 +38,7 @@ export const useAuthStore = defineStore('auth', () => {
// Передаем store объект с loading и error
return withLoading({ loading, error }, async () => {
await apiClient.post('/auth/register', userData)
// Auto-login after registration
const loginResponse = await apiClient.post('/auth/login', {
email: userData.email,
@@ -95,7 +96,7 @@ export const useAuthStore = defineStore('auth', () => {
const initializeAuth = async () => {
if (initialized.value || !token.value) return
initialized.value = true
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 {
// State
user,
@@ -126,6 +168,8 @@ export const useAuthStore = defineStore('auth', () => {
fetchProfile,
updateProfile,
initializeAuth,
clearAuth
clearAuth,
updateAvatar,
deleteAvatar
}
})
+1 -1
View File
@@ -6,7 +6,7 @@
<div v-else-if="user" class="profile-content">
<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>
<p>Участник с {{ joinDate }}</p>
<p class="user-email">{{ user.email }}</p>
@@ -2,6 +2,11 @@
<div class="page">
<h1> Редактирование профиля</h1>
<div class="avatar-section">
<h3>Фотография профиля</h3>
<AvatarUpload :user="user" :show-actions="true" />
</div>
<div v-if="loading && !user" class="loading">Загрузка...</div>
<form v-else @submit.prevent="handleSubmit" class="profile-edit-form">
@@ -90,9 +95,13 @@
<script>
import { useAuthStore } from '../stores/auth'
import AvatarUpload from '../components/AvatarUpload.vue'
export default {
name: 'ProfileEdit',
components: {
AvatarUpload
},
setup() {
const authStore = useAuthStore()
return { authStore }
+23
View File
@@ -12,15 +12,38 @@ 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/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.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/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
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/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
require (
github.com/gin-gonic/gin v1.11.0
github.com/go-playground/validator/v10 v10.28.0
github.com/jackc/pgpassfile v1.0.0 // 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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
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/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
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/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/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
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/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/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.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.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/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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
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/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/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/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+69 -71
View File
@@ -21,7 +21,7 @@ import (
type AuthHandler struct {
authService service.AuthService
jwtService service.JWTService
logger logger.Interface
logger logger.LoggerInterface
}
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) {
h.logger.Info("handling login request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
h.logger.Info("handling login request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем Content-Type
if r.Header.Get("Content-Type") != "application/json" {
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")
return
}
// Проверяем Content-Type
if r.Header.Get("Content-Type") != "application/json" {
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")
return
}
// Читаем и логируем тело запроса
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Error("failed to read request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
return
}
defer r.Body.Close()
// Читаем и логируем тело запроса
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Error("failed to read request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
return
}
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
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("JSON decode failed",
zap.Error(err),
zap.String("raw_body", string(bodyBytes)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("JSON decode failed",
zap.Error(err),
zap.String("raw_body", string(bodyBytes)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
req.Email = strings.TrimSpace(req.Email)
req.Password = strings.TrimSpace(req.Password)
req.Email = strings.TrimSpace(req.Email)
req.Password = strings.TrimSpace(req.Password)
// Валидация
if req.Email == "" || req.Password == "" {
h.logger.Warn("validation failed",
zap.String("email", req.Email),
zap.Int("password_len", len(req.Password)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
return
}
// Валидация
if req.Email == "" || req.Password == "" {
h.logger.Warn("validation failed",
zap.String("email", req.Email),
zap.Int("password_len", len(req.Password)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
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)
if err != nil {
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
return
}
user, token, err := h.authService.Login(req.Email, req.Password)
if err != nil {
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
return
}
// Устанавливаем куки
http.SetCookie(w, &http.Cookie{
Name: "auth_token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour),
})
// Устанавливаем куки
http.SetCookie(w, &http.Cookie{
Name: "auth_token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour),
})
h.logger.Info("login successful",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
)
h.logger.Info("login successful",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Login successful",
"token": token,
"user": toUserResponse(user),
})
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Login successful",
"token": token,
"user": toUserResponse(user),
})
}
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",
})
}
@@ -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 {
newsService service.NewsService
logger logger.Interface
validator *validator.Validate
newsService service.NewsService
logger logger.LoggerInterface
validator *validator.Validate
}
func NewNewsHandler(newsService service.NewsService, log logger.Interface) *NewsHandler {
return &NewsHandler{
newsService: newsService,
logger: log,
validator: validator.New(),
}
func NewNewsHandler(newsService service.NewsService, log logger.LoggerInterface) *NewsHandler {
return &NewsHandler{
newsService: newsService,
logger: log,
validator: validator.New(),
}
}
// GetNews возвращает список новостей с пагинацией и фильтрацией
func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
category := r.URL.Query().Get("category")
if limit == 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
news, total, err := h.newsService.GetAllNews(limit, offset, category)
if err != nil {
h.logger.Error("Failed to get news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news,
"total": total,
"limit": limit,
"offset": offset,
})
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
category := r.URL.Query().Get("category")
if limit == 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
news, total, err := h.newsService.GetAllNews(limit, offset, category)
if err != nil {
h.logger.Error("Failed to get news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news,
"total": total,
"limit": limit,
"offset": offset,
})
}
// GetNewsByID возвращает конкретную новость
func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
news, err := h.newsService.GetNewsByID(uint(id))
if err != nil {
utils.RespondWithError(w, http.StatusNotFound, "News not found")
return
}
utils.RespondWithJSON(w, http.StatusOK, news)
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
news, err := h.newsService.GetNewsByID(uint(id))
if err != nil {
utils.RespondWithError(w, http.StatusNotFound, "News not found")
return
}
utils.RespondWithJSON(w, http.StatusOK, news)
}
// CreateNews создает новую новость
func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var req models.CreateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
news, err := h.newsService.CreateNews(req, userID)
if err != nil {
h.logger.Error("Failed to create news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
return
}
utils.RespondWithJSON(w, http.StatusCreated, news)
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var req models.CreateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
news, err := h.newsService.CreateNews(req, userID)
if err != nil {
h.logger.Error("Failed to create news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
return
}
utils.RespondWithJSON(w, http.StatusCreated, news)
}
// UpdateNews обновляет новость
func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
var req models.UpdateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
news, err := h.newsService.UpdateNews(uint(id), req, userID)
if err != nil {
if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
return
}
utils.RespondWithJSON(w, http.StatusOK, news)
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
var req models.UpdateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
news, err := h.newsService.UpdateNews(uint(id), req, userID)
if err != nil {
if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
return
}
utils.RespondWithJSON(w, http.StatusOK, news)
}
// DeleteNews удаляет новость
func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
err = h.newsService.DeleteNews(uint(id), userID)
if err != nil {
if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
err = h.newsService.DeleteNews(uint(id), userID)
if err != nil {
if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
}
// CreateComment создает комментарий к новости
func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
newsIDStr := chi.URLParam(r, "id")
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
var req models.CreateCommentRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
return
}
utils.RespondWithJSON(w, http.StatusCreated, comment)
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
newsIDStr := chi.URLParam(r, "id")
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
var req models.CreateCommentRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
if err := h.validator.Struct(req); err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
return
}
utils.RespondWithJSON(w, http.StatusCreated, comment)
}
// GetComments возвращает комментарии к новости
func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) {
newsIDStr := chi.URLParam(r, "id")
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
return
}
utils.RespondWithJSON(w, http.StatusOK, comments)
newsIDStr := chi.URLParam(r, "id")
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
return
}
utils.RespondWithJSON(w, http.StatusOK, comments)
}
// DeleteComment удаляет комментарий
func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
commentIDStr := chi.URLParam(r, "commentId")
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
return
}
err = h.newsService.DeleteComment(uint(commentID), userID)
if err != nil {
if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
commentIDStr := chi.URLParam(r, "commentId")
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
return
}
err = h.newsService.DeleteComment(uint(commentID), userID)
if err != nil {
if err.Error() == "access denied" {
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
}
// GetUserNews возвращает новости конкретного пользователя
func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit == 0 {
limit = 10
}
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news,
"total": total,
})
}
userID, ok := r.Context().Value("userID").(uint)
if !ok {
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit == 0 {
limit = 10
}
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
if err != nil {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
return
}
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news,
"total": total,
})
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
)
type UserHandler struct {
logger logger.Interface
logger logger.LoggerInterface
userService service.UserService
}
+16 -13
View File
@@ -8,26 +8,29 @@ import (
"gorm.io/gorm"
)
// models/user.go - добавить поле Avatar
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
Role string `json:"role" gorm:"default:user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"`
Avatar string `json:"avatar"` // Путь к файлу аватара
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
Role string `json:"role" gorm:"default:user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
type UserUpdate struct {
ID uint `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"` // Добавить поле аватара
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
@@ -8,66 +8,78 @@ import (
)
type UserRepository interface {
Create(user *models.User) error
FindByID(id uint) (*models.User, error)
FindByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
UpdateExcludeEmail(userUpdate *models.User) error
Create(user *models.User) error
FindByID(id uint) (*models.User, error)
FindByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) 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 {
db *gorm.DB
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
return &userRepository{db: db}
}
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) {
var user models.User
err := r.db.First(&user, id).Error
return &user, err
var user models.User
err := r.db.First(&user, id).Error
return &user, err
}
func (r *userRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
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 {
return r.db.Delete(&models.User{}, id).Error
return r.db.Delete(&models.User{}, id).Error
}
// repository/user_repository.go
func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error {
// Обновляем только разрешенные поля
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
"first_name": userUpdate.FirstName,
"last_name": userUpdate.LastName,
"phone": userUpdate.Phone,
"experience": userUpdate.Experience,
"goals": userUpdate.Goals,
"newsletter": userUpdate.Newsletter,
"updated_at": userUpdate.UpdatedAt,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
}
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
"first_name": userUpdate.FirstName,
"last_name": userUpdate.LastName,
"avatar": userUpdate.Avatar, // Добавить обновление аватара
"phone": userUpdate.Phone,
"experience": userUpdate.Experience,
"goals": userUpdate.Goals,
"newsletter": userUpdate.Newsletter,
"updated_at": userUpdate.UpdatedAt,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
}
+10 -1
View File
@@ -11,7 +11,7 @@ import (
"api_bb/internal/handlers"
"api_bb/internal/repository"
"api_bb/internal/service"
"api_bb/pkg/logger" // Добавьте импорт логгера
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
)
@@ -36,12 +36,14 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер
userService := service.NewUserService(userRepo, jwtService, baseLogger)
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
avatarService := service.NewAvatarService(userRepo, baseLogger)
// Initialize handlers
healthHandler := handlers.NewHealthHandler()
authHandler := handlers.NewAuthHandler(authService, jwtService)
userHandler := handlers.NewUserHandler(&userService)
newsHandler := handlers.NewNewsHandler(newsService, baseLogger)
avatarHandler := handlers.NewAvatarHandler(avatarService)
// Health 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.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())
})
// Логируем все зарегистрированные маршруты
routeLogger := logger.NewRouteLogger(baseLogger)
routeLogger.LogRoutes(r)
return r
}
@@ -20,10 +20,10 @@ type AuthService interface {
type authService struct {
userRepo repository.UserRepository
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"))
@@ -34,7 +34,6 @@ func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, l
}
}
func (s *authService) Register(user *models.User) error {
s.logger.Info("Registering new user",
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 {
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
GetNewsByID(id uint) (*models.NewsResponse, error)
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
DeleteNews(id uint, userID uint) error
IncrementViews(id uint) error
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
DeleteComment(commentID, userID uint) error
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
GetNewsByID(id uint) (*models.NewsResponse, error)
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
DeleteNews(id uint, userID uint) error
IncrementViews(id uint) error
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
DeleteComment(commentID, userID uint) error
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
}
type newsService struct {
newsRepo repository.NewsRepository
commentRepo repository.CommentRepository
logger logger.Interface
newsRepo repository.NewsRepository
commentRepo repository.CommentRepository
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"))
return &newsService{
newsRepo: newsRepo,
commentRepo: commentRepo,
logger: serviceLogger,
}
newsRepo: newsRepo,
commentRepo: commentRepo,
logger: serviceLogger,
}
}
func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) {
news := &models.News{
Title: req.Title,
Excerpt: req.Excerpt,
Content: req.Content,
Image: req.Image,
Category: req.Category,
AuthorID: authorID,
}
if err := s.newsRepo.Create(news); err != nil {
s.logger.Error("Failed to create news", zap.Error(err))
return nil, errors.New("failed to create news")
}
// Получаем созданную новость с автором
createdNews, err := s.newsRepo.GetByID(news.ID)
if err != nil {
return nil, err
}
return s.toNewsResponse(createdNews), nil
news := &models.News{
Title: req.Title,
Excerpt: req.Excerpt,
Content: req.Content,
Image: req.Image,
Category: req.Category,
AuthorID: authorID,
}
if err := s.newsRepo.Create(news); err != nil {
s.logger.Error("Failed to create news", zap.Error(err))
return nil, errors.New("failed to create news")
}
// Получаем созданную новость с автором
createdNews, err := s.newsRepo.GetByID(news.ID)
if err != nil {
return nil, err
}
return s.toNewsResponse(createdNews), nil
}
func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) {
news, err := s.newsRepo.GetByID(id)
if err != nil {
return nil, errors.New("news not found")
}
// Увеличиваем счетчик просмотров
go s.newsRepo.IncrementViews(id)
return s.toNewsResponse(news), nil
news, err := s.newsRepo.GetByID(id)
if err != nil {
return nil, errors.New("news not found")
}
// Увеличиваем счетчик просмотров
go s.newsRepo.IncrementViews(id)
return s.toNewsResponse(news), nil
}
func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) {
news, total, err := s.newsRepo.GetAll(limit, offset, category)
if err != nil {
return nil, 0, err
}
responses := make([]models.NewsResponse, len(news))
for i, n := range news {
responses[i] = *s.toNewsResponse(&n)
}
return responses, total, nil
news, total, err := s.newsRepo.GetAll(limit, offset, category)
if err != nil {
return nil, 0, err
}
responses := make([]models.NewsResponse, len(news))
for i, n := range news {
responses[i] = *s.toNewsResponse(&n)
}
return responses, total, nil
}
func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) {
news, err := s.newsRepo.GetByID(id)
if err != nil {
return nil, errors.New("news not found")
}
// Проверяем права доступа
if news.AuthorID != userID {
return nil, errors.New("access denied")
}
// Обновляем поля
if req.Title != "" {
news.Title = req.Title
}
if req.Excerpt != "" {
news.Excerpt = req.Excerpt
}
if req.Content != "" {
news.Content = req.Content
}
if req.Image != "" {
news.Image = req.Image
}
if req.Category != "" {
news.Category = req.Category
}
if err := s.newsRepo.Update(news); err != nil {
return nil, err
}
return s.toNewsResponse(news), nil
news, err := s.newsRepo.GetByID(id)
if err != nil {
return nil, errors.New("news not found")
}
// Проверяем права доступа
if news.AuthorID != userID {
return nil, errors.New("access denied")
}
// Обновляем поля
if req.Title != "" {
news.Title = req.Title
}
if req.Excerpt != "" {
news.Excerpt = req.Excerpt
}
if req.Content != "" {
news.Content = req.Content
}
if req.Image != "" {
news.Image = req.Image
}
if req.Category != "" {
news.Category = req.Category
}
if err := s.newsRepo.Update(news); err != nil {
return nil, err
}
return s.toNewsResponse(news), nil
}
func (s *newsService) DeleteNews(id uint, userID uint) error {
news, err := s.newsRepo.GetByID(id)
if err != nil {
return errors.New("news not found")
}
// Проверяем права доступа
if news.AuthorID != userID {
return errors.New("access denied")
}
return s.newsRepo.Delete(id)
news, err := s.newsRepo.GetByID(id)
if err != nil {
return errors.New("news not found")
}
// Проверяем права доступа
if news.AuthorID != userID {
return errors.New("access denied")
}
return s.newsRepo.Delete(id)
}
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) {
// Проверяем существование новости
_, err := s.newsRepo.GetByID(newsID)
if err != nil {
return nil, errors.New("news not found")
}
comment := &models.Comment{
Content: req.Content,
NewsID: newsID,
AuthorID: authorID,
}
if err := s.commentRepo.Create(comment); err != nil {
return nil, err
}
// Получаем созданный комментарий с автором
createdComment, err := s.commentRepo.GetByID(comment.ID)
if err != nil {
return nil, err
}
return s.toCommentResponse(createdComment), nil
// Проверяем существование новости
_, err := s.newsRepo.GetByID(newsID)
if err != nil {
return nil, errors.New("news not found")
}
comment := &models.Comment{
Content: req.Content,
NewsID: newsID,
AuthorID: authorID,
}
if err := s.commentRepo.Create(comment); err != nil {
return nil, err
}
// Получаем созданный комментарий с автором
createdComment, err := s.commentRepo.GetByID(comment.ID)
if err != nil {
return nil, err
}
return s.toCommentResponse(createdComment), nil
}
func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) {
comments, err := s.commentRepo.GetByNewsID(newsID)
if err != nil {
return nil, err
}
responses := make([]models.CommentResponse, len(comments))
for i, c := range comments {
responses[i] = *s.toCommentResponse(&c)
}
return responses, nil
comments, err := s.commentRepo.GetByNewsID(newsID)
if err != nil {
return nil, err
}
responses := make([]models.CommentResponse, len(comments))
for i, c := range comments {
responses[i] = *s.toCommentResponse(&c)
}
return responses, nil
}
func (s *newsService) DeleteComment(commentID, userID uint) error {
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
return errors.New("comment not found")
}
// Проверяем права доступа
if comment.AuthorID != userID {
return errors.New("access denied")
}
return s.commentRepo.Delete(commentID)
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
return errors.New("comment not found")
}
// Проверяем права доступа
if comment.AuthorID != userID {
return errors.New("access denied")
}
return s.commentRepo.Delete(commentID)
}
func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) {
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
if err != nil {
return nil, 0, err
}
responses := make([]models.NewsResponse, len(news))
for i, n := range news {
responses[i] = *s.toNewsResponse(&n)
}
return responses, total, nil
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
if err != nil {
return nil, 0, err
}
responses := make([]models.NewsResponse, len(news))
for i, n := range news {
responses[i] = *s.toNewsResponse(&n)
}
return responses, total, nil
}
// Вспомогательные методы для преобразования
func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse {
return &models.NewsResponse{
ID: news.ID,
CreatedAt: news.CreatedAt,
UpdatedAt: news.UpdatedAt,
Title: news.Title,
Excerpt: news.Excerpt,
Content: news.Content,
Image: news.Image,
Category: news.Category,
Views: news.Views,
Author: models.AuthorInfo{
ID: news.Author.ID,
FirstName: news.Author.FirstName,
LastName: news.Author.LastName,
},
Comments: len(news.Comments),
}
return &models.NewsResponse{
ID: news.ID,
CreatedAt: news.CreatedAt,
UpdatedAt: news.UpdatedAt,
Title: news.Title,
Excerpt: news.Excerpt,
Content: news.Content,
Image: news.Image,
Category: news.Category,
Views: news.Views,
Author: models.AuthorInfo{
ID: news.Author.ID,
FirstName: news.Author.FirstName,
LastName: news.Author.LastName,
},
Comments: len(news.Comments),
}
}
func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse {
return &models.CommentResponse{
ID: comment.ID,
CreatedAt: comment.CreatedAt,
Content: comment.Content,
Author: models.AuthorInfo{
ID: comment.Author.ID,
FirstName: comment.Author.FirstName,
LastName: comment.Author.LastName,
},
}
}
return &models.CommentResponse{
ID: comment.ID,
CreatedAt: comment.CreatedAt,
Content: comment.Content,
Author: models.AuthorInfo{
ID: comment.Author.ID,
FirstName: comment.Author.FirstName,
LastName: comment.Author.LastName,
},
}
}
@@ -18,16 +18,15 @@ type UserService interface {
type userService struct {
userRepo repository.UserRepository
jwtService JWTService
logger logger.Interface
logger logger.LoggerInterface
}
// UpdateProfile implements UserService.
func (s userService) UpdateProfile(user *models.User) error {
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"))
+31 -31
View File
@@ -3,73 +3,73 @@ package logger
import "go.uber.org/zap"
// Interface определяет контракт для логгера
type Interface interface {
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
Fatal(msg string, fields ...zap.Field)
Debugf(template string, args ...interface{})
Infof(template string, args ...interface{})
Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{})
With(fields ...zap.Field) Interface
// LoggerInterface определяет контракт для логгера
type LoggerInterface interface {
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
Fatal(msg string, fields ...zap.Field)
Debugf(template string, args ...interface{})
Infof(template string, args ...interface{})
Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{})
With(fields ...zap.Field) LoggerInterface
}
// wrapper обертка для zap.Logger
type wrapper struct {
logger *zap.Logger
logger *zap.Logger
}
// NewWrapper создает новую обертку
func NewWrapper(logger *zap.Logger) Interface {
return &wrapper{logger: logger}
func NewWrapper(logger *zap.Logger) LoggerInterface {
return &wrapper{logger: logger}
}
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) {
w.logger.Info(msg, fields...)
w.logger.Info(msg, fields...)
}
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) {
w.logger.Error(msg, fields...)
w.logger.Error(msg, fields...)
}
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{}) {
w.logger.Sugar().Debugf(template, args...)
w.logger.Sugar().Debugf(template, args...)
}
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{}) {
w.logger.Sugar().Warnf(template, args...)
w.logger.Sugar().Warnf(template, args...)
}
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{}) {
w.logger.Sugar().Fatalf(template, args...)
w.logger.Sugar().Fatalf(template, args...)
}
func (w *wrapper) With(fields ...zap.Field) Interface {
return &wrapper{logger: w.logger.With(fields...)}
}
func (w *wrapper) With(fields ...zap.Field) LoggerInterface {
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
}