diff --git a/cmd/client/main.go b/cmd/client/main.go index fc8c493..81cbc0a 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -13,6 +13,7 @@ func main() { http.HandleFunc("/dashboard", handlers.AuthMiddleware(handlers.DashboardHandler)) + http.HandleFunc("/upload", handlers.AuthMiddleware(handlers.UploadHandler)) // Запуск сервера log.Println("Клиент запущен на :8080") log.Fatal(http.ListenAndServe(":8080", nil)) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4757d6d --- /dev/null +++ b/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "log" + "os" +) + +var ( + // PocketBaseURL URL сервера PocketBase + PocketBaseURL string +) + +func init() { + // Получаем URL из переменной окружения или используем значение по умолчанию + PocketBaseURL = os.Getenv("POCKETBASE_URL") + if PocketBaseURL == "" { + PocketBaseURL = "http://localhost:8090" + log.Printf("POCKETBASE_URL не установлен, используем значение по умолчанию: %s", PocketBaseURL) + } else { + log.Printf("Используем POCKETBASE_URL из переменной окружения: %s", PocketBaseURL) + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 7005d83..72241d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ - services: server: build: @@ -14,3 +13,5 @@ services: - server ports: - "8080:8080" # Укажите порт клиента + environment: + - POCKETBASE_URL=http://server:8090 diff --git a/handlers/dashboard.go b/handlers/dashboard.go index f9362f7..17db275 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -2,13 +2,15 @@ package handlers import ( "encoding/json" + "fmt" "io" "log" "net/http" + "pocketbaseSigner/config" "pocketbaseSigner/models" + "time" ) - func DashboardHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed) @@ -31,7 +33,7 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { } // Получаем данные пользователя - url := "http://localhost:8090/api/collections/users/records/" + userIdCookie.Value + url := config.PocketBaseURL + "/api/collections/users/records/" + userIdCookie.Value req, err := http.NewRequest("GET", url, nil) if err != nil { log.Printf("Ошибка при создании запроса: %v", err) @@ -87,7 +89,7 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { padding: 20px; } .container { - max-width: 800px; + max-width: 1200px; margin: 0 auto; background: white; padding: 20px; @@ -114,6 +116,87 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { .logout-btn:hover { background: #c82333; } + .files-section { + margin-top: 30px; + } + .files-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 20px; + margin-top: 20px; + } + .file-card { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 15px; + cursor: pointer; + transition: transform 0.2s; + } + .file-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + } + .file-name { + font-weight: bold; + margin-bottom: 10px; + word-break: break-word; + } + .file-date { + color: #6c757d; + font-size: 0.9em; + } + .pdf-viewer { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + display: none; + z-index: 1000; + } + .pdf-viewer iframe { + width: 90%; + height: 90%; + margin: 2% auto; + display: block; + background: white; + border: none; + border-radius: 8px; + } + .close-viewer { + position: absolute; + top: 20px; + right: 20px; + background: white; + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + .upload-section { + margin-top: 20px; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; + } + .upload-btn { + background: #28a745; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + } + .upload-btn:hover { + background: #218838; + } @@ -130,17 +213,124 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) {

Имя: ` + userData.FirstName + `

Фамилия: ` + userData.LastName + `

Телефон: ` + userData.Phone + `

-

Дата регистрации: ` + userData.Created + `

-

Последнее обновление: ` + userData.Updated + `

-

Статус верификации: ` + formatVerified(userData.Verified) + `

+ + +
+

Мои документы

+
+
+ + +
+
+
+ ` + generateFilesGridFromNames(userData.Files, userIdCookie.Value, cookie.Value) + ` +
+ +
+ + +
+ + ` w.Write([]byte(dashboardHTML)) } +func getFileToken(cookie string) (string, error) { + url := config.PocketBaseURL + "/api/files/token" + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", cookie) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("ошибка получения токена: %d", resp.StatusCode) + } + + var result map[string]string + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", err + } + + return result["token"], nil +} + +func generateFilesGridFromNames(files []string, userId string, authCookie string) string { + if len(files) == 0 { + return `
У вас пока нет загруженных документов
` + } + + // Получаем токен для доступа к файлам + token, err := getFileToken(authCookie) + if err != nil { + log.Printf("Ошибка при получении токена файла: %v", err) + return `
Ошибка при получении доступа к файлам
` + } + + var html string + for _, fileName := range files { + // Формируем URL для просмотра файла с токеном + fileUrl := config.PocketBaseURL + "/api/files/_pb_users_auth_/" + userId + "/" + fileName + "?token=" + token + html += ` +
+
+ + + + + + + +
+
+
` + fileName + `
+
+
+ ` + } + return html +} + +func formatDate(dateStr string) string { + // Парсим дату из формата ISO 8601 + t, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return dateStr + } + // Форматируем в удобный для чтения формат + return t.Format("02.01.2006 15:04") +} + func formatVerified(verified bool) string { if verified { return "Подтвержден" diff --git a/handlers/login.go b/handlers/login.go index 9d89539..4d3ccb4 100644 --- a/handlers/login.go +++ b/handlers/login.go @@ -6,6 +6,7 @@ import ( "io" "log" "net/http" + "pocketbaseSigner/config" "pocketbaseSigner/models" ) @@ -13,7 +14,6 @@ import ( //2. реализовать логин //3. приступить к миддлвейру - func LoginHandler(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { http.ServeFile(w, r, "./web/login_form.html") @@ -27,28 +27,35 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } - if r.FormValue("email") == "" || r.FormValue("password") == "" { + user := models.LoginForm{ + Email: r.FormValue("email"), + Password: r.FormValue("password"), + } + + // Проверяем обязательные поля + if user.Email == "" || user.Password == "" { http.Error(w, "Email и пароль обязательны!", http.StatusBadRequest) return } - // Создаем структуру для запроса - loginData := map[string]string{ - "identity": r.FormValue("email"), - "password": r.FormValue("password"), + // Подготавливаем данные для отправки + dataMap := map[string]string{ + "identity": user.Email, + "password": user.Password, } - // Преобразуем в JSON - jsonData, err := json.Marshal(loginData) + data, err := json.Marshal(dataMap) if err != nil { log.Printf("Ошибка при создании JSON: %v", err) http.Error(w, "Внутренняя ошибка сервера", http.StatusInternalServerError) return } - url := "http://localhost:8090/api/collections/users/auth-with-password" + log.Printf("Отправляем данные пользователя: %+v\n", user) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + url := config.PocketBaseURL + "/api/collections/users/auth-with-password" + log.Printf("URL: %v\n", url) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) if err != nil { log.Printf("Ошибка при создании запроса: %v", err) http.Error(w, "Внутренняя ошибка сервера", http.StatusInternalServerError) diff --git a/handlers/middleware.go b/handlers/middleware.go index 861b6b1..cb2ba55 100644 --- a/handlers/middleware.go +++ b/handlers/middleware.go @@ -3,6 +3,7 @@ package handlers import ( "log" "net/http" + "pocketbaseSigner/config" ) func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { @@ -16,7 +17,8 @@ func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { } // Проверяем валидность токена через PocketBase - req, err := http.NewRequest("POST", "http://localhost:8090/api/collections/users/auth-refresh", nil) + url := config.PocketBaseURL + "/api/collections/users/auth-refresh" + req, err := http.NewRequest("POST", url, nil) if err != nil { log.Printf("Ошибка при создании запроса: %v", err) http.Error(w, "Внутренняя ошибка сервера", http.StatusInternalServerError) diff --git a/handlers/register.go b/handlers/register.go index 32d6c7b..466a8fa 100644 --- a/handlers/register.go +++ b/handlers/register.go @@ -5,6 +5,7 @@ import ( "encoding/json" "log" "net/http" + "pocketbaseSigner/config" "pocketbaseSigner/models" ) @@ -61,7 +62,7 @@ func RegisterFormHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Отправляем данные пользователя: %+v\n", user) - url := "http://localhost:8090/api/collections/users/records" + url := config.PocketBaseURL + "/api/collections/users/records" req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) if err != nil { log.Printf("Ошибка при создании запроса: %v", err) diff --git a/handlers/upload.go b/handlers/upload.go new file mode 100644 index 0000000..4ddad1b --- /dev/null +++ b/handlers/upload.go @@ -0,0 +1,171 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "io" + "log" + "mime/multipart" + "net/http" + "pocketbaseSigner/config" + "pocketbaseSigner/models" +) + +func UploadHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed) + return + } + + // Получаем токен из cookie + cookie, err := r.Cookie("pb_auth") + if err != nil { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + // Получаем ID пользователя из cookie + userIdCookie, err := r.Cookie("user_id") + if err != nil { + log.Printf("Ошибка при получении ID пользователя: %v", err) + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + // Получаем текущие данные пользователя + url := config.PocketBaseURL + "/api/collections/users/records/" + userIdCookie.Value + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("Ошибка при создании запроса: %v", err) + http.Error(w, "Внутренняя ошибка сервера", http.StatusInternalServerError) + return + } + + req.Header.Set("Authorization", cookie.Value) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Ошибка при получении данных пользователя: %v", err) + http.Error(w, "Ошибка при получении данных", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Ошибка при чтении ответа: %v", err) + http.Error(w, "Ошибка при обработке данных", http.StatusInternalServerError) + return + } + + var userData models.UserData + if err := json.Unmarshal(body, &userData); err != nil { + log.Printf("Ошибка при разборе JSON: %v", err) + http.Error(w, "Ошибка при обработке данных", http.StatusInternalServerError) + return + } + + // Получаем файл из формы + file, header, err := r.FormFile("files") + if err != nil { + log.Printf("Ошибка при получении файла: %v", err) + http.Error(w, "Ошибка при загрузке файла", http.StatusBadRequest) + return + } + defer file.Close() + + // Создаем multipart writer для отправки файла + requestBody := &bytes.Buffer{} + writer := multipart.NewWriter(requestBody) + + // Добавляем файл + part, err := writer.CreateFormFile("files", header.Filename) + if err != nil { + log.Printf("Ошибка при создании формы: %v", err) + http.Error(w, "Ошибка при подготовке файла", http.StatusInternalServerError) + return + } + + // Копируем содержимое файла + _, err = io.Copy(part, file) + if err != nil { + log.Printf("Ошибка при копировании файла: %v", err) + http.Error(w, "Ошибка при обработке файла", http.StatusInternalServerError) + return + } + + // Добавляем существующие файлы + for _, existingFile := range userData.Files { + part, err := writer.CreateFormFile("files", existingFile) + if err != nil { + log.Printf("Ошибка при добавлении существующего файла: %v", err) + continue + } + // Получаем содержимое существующего файла + fileUrl := config.PocketBaseURL + "/api/files/_pb_users_auth_/" + userIdCookie.Value + "/" + existingFile + fileReq, err := http.NewRequest("GET", fileUrl, nil) + if err != nil { + log.Printf("Ошибка при создании запроса для существующего файла: %v", err) + continue + } + fileReq.Header.Set("Authorization", cookie.Value) + fileResp, err := client.Do(fileReq) + if err != nil { + log.Printf("Ошибка при получении существующего файла: %v", err) + continue + } + _, err = io.Copy(part, fileResp.Body) + fileResp.Body.Close() + if err != nil { + log.Printf("Ошибка при копировании существующего файла: %v", err) + continue + } + } + + // Закрываем writer + err = writer.Close() + if err != nil { + log.Printf("Ошибка при закрытии writer: %v", err) + http.Error(w, "Ошибка при подготовке запроса", http.StatusInternalServerError) + return + } + + // Создаем запрос к PocketBase + url = config.PocketBaseURL + "/api/collections/users/records/" + userIdCookie.Value + log.Printf("Отправка запроса на URL: %s", url) + log.Printf("Content-Type: %s", writer.FormDataContentType()) + + req, err = http.NewRequest("PATCH", url, requestBody) + if err != nil { + log.Printf("Ошибка при создании запроса: %v", err) + http.Error(w, "Ошибка при отправке файла", http.StatusInternalServerError) + return + } + + // Устанавливаем заголовки + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", cookie.Value) + + // Отправляем запрос + resp, err = client.Do(req) + if err != nil { + log.Printf("Ошибка при отправке запроса: %v", err) + http.Error(w, "Ошибка при загрузке файла", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + // Читаем ответ + respBody, _ := io.ReadAll(resp.Body) + log.Printf("Ответ сервера (статус %d): %s", resp.StatusCode, string(respBody)) + + // Проверяем ответ + if resp.StatusCode != http.StatusOK { + log.Printf("Ошибка при загрузке файла (статус %d): %s", resp.StatusCode, string(respBody)) + http.Error(w, "Ошибка при загрузке файла", resp.StatusCode) + return + } + + // Перенаправляем обратно на дашборд + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) +} diff --git a/models/files.go b/models/files.go new file mode 100644 index 0000000..fed0981 --- /dev/null +++ b/models/files.go @@ -0,0 +1,19 @@ +package models + +// File представляет информацию о файле +type File struct { + ID string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + Created string `json:"created"` + Updated string `json:"updated"` + User string `json:"user"` +} + +// FilesList представляет список файлов +type FilesList struct { + Page int `json:"page"` + PerPage int `json:"perPage"` + Total int `json:"total"` + Items []File `json:"items"` +} diff --git a/models/user.go b/models/user.go index f5e8d9d..1a1585c 100644 --- a/models/user.go +++ b/models/user.go @@ -1,6 +1,5 @@ package models - type UserResponse struct { ID string `json:"id"` Email string `json:"email"` @@ -8,17 +7,19 @@ type UserResponse struct { LastName string `json:"last_name"` } +// UserData представляет данные пользователя type UserData struct { - CollectionId string `json:"collectionId"` - CollectionName string `json:"collectionName"` - ID string `json:"id"` - Email string `json:"email"` - FirstName string `json:"FirstName"` - LastName string `json:"LastName"` - Phone string `json:"Phone"` - EmailVisibility bool `json:"emailVisibility"` - Verified bool `json:"verified"` - Avatar string `json:"avatar"` - Created string `json:"created"` - Updated string `json:"updated"` -} \ No newline at end of file + CollectionId string `json:"collectionId"` + CollectionName string `json:"collectionName"` + ID string `json:"id"` + Email string `json:"email"` + Password string `json:"password"` + PasswordConfirm string `json:"passwordConfirm"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Phone string `json:"phone"` + Created string `json:"created"` + Updated string `json:"updated"` + Verified bool `json:"verified"` + Files []string `json:"files"` +} diff --git a/pb_data/auxiliary.db b/pb_data/auxiliary.db index e86746f..69f5abb 100644 Binary files a/pb_data/auxiliary.db and b/pb_data/auxiliary.db differ diff --git a/pb_data/data.db b/pb_data/data.db index f5beb1f..a136e96 100644 Binary files a/pb_data/data.db and b/pb_data/data.db differ diff --git a/pb_data/storage/_pb_users_auth_/54m99pn0exn3r5r/design_db_pwfofsm6bc_5csswuyjgw.pdf b/pb_data/storage/_pb_users_auth_/54m99pn0exn3r5r/design_db_pwfofsm6bc_5csswuyjgw.pdf new file mode 100644 index 0000000..48cf9be Binary files /dev/null and b/pb_data/storage/_pb_users_auth_/54m99pn0exn3r5r/design_db_pwfofsm6bc_5csswuyjgw.pdf differ diff --git a/pb_data/storage/_pb_users_auth_/54m99pn0exn3r5r/design_db_pwfofsm6bc_5csswuyjgw.pdf.attrs b/pb_data/storage/_pb_users_auth_/54m99pn0exn3r5r/design_db_pwfofsm6bc_5csswuyjgw.pdf.attrs new file mode 100644 index 0000000..8e5221e --- /dev/null +++ b/pb_data/storage/_pb_users_auth_/54m99pn0exn3r5r/design_db_pwfofsm6bc_5csswuyjgw.pdf.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"application/pdf","user.metadata":{"original-filename":"design_db_pwfofsm6bc.pdf"},"md5":"EGV9XOunjL+DZzoKvFJqHw=="} diff --git a/web/login_form.html b/web/login_form.html index 9bbcec9..f6a30d7 100644 --- a/web/login_form.html +++ b/web/login_form.html @@ -57,6 +57,19 @@ button:hover { background: #0056b3; } + .register-link { + text-align: center; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #eee; + } + .register-link a { + color: #007bff; + text-decoration: none; + } + .register-link a:hover { + text-decoration: underline; + } @@ -78,6 +91,9 @@ +