Дашборд доступно добавление файлов

This commit is contained in:
Alexander 2025-05-12 18:42:03 +03:00
parent 76a770add8
commit 11431a256f
15 changed files with 465 additions and 33 deletions

View File

@ -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))

22
config/config.go Normal file
View File

@ -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)
}
}

View File

@ -1,4 +1,3 @@
services:
server:
build:
@ -14,3 +13,5 @@ services:
- server
ports:
- "8080:8080" # Укажите порт клиента
environment:
- POCKETBASE_URL=http://server:8090

View File

@ -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;
}
</style>
</head>
<body>
@ -130,17 +213,124 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) {
<p><strong>Имя:</strong> ` + userData.FirstName + `</p>
<p><strong>Фамилия:</strong> ` + userData.LastName + `</p>
<p><strong>Телефон:</strong> ` + userData.Phone + `</p>
<p><strong>Дата регистрации:</strong> ` + userData.Created + `</p>
<p><strong>Последнее обновление:</strong> ` + userData.Updated + `</p>
<p><strong>Статус верификации:</strong> ` + formatVerified(userData.Verified) + `</p>
</div>
<div class="files-section">
<h2>Мои документы</h2>
<div class="upload-section">
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="files" accept=".pdf" required>
<button type="submit" class="upload-btn">Загрузить PDF</button>
</form>
</div>
<div class="files-grid">
` + generateFilesGridFromNames(userData.Files, userIdCookie.Value, cookie.Value) + `
</div>
</div>
</div>
<div class="pdf-viewer" id="pdfViewer">
<button class="close-viewer" onclick="closePdfViewer()">×</button>
<iframe id="pdfFrame" src=""></iframe>
</div>
<script>
function openPdfViewer(url) {
document.getElementById('pdfFrame').src = url;
document.getElementById('pdfViewer').style.display = 'block';
}
function closePdfViewer() {
document.getElementById('pdfViewer').style.display = 'none';
document.getElementById('pdfFrame').src = '';
}
// Закрытие по клику вне iframe
document.getElementById('pdfViewer').addEventListener('click', function(e) {
if (e.target === this) {
closePdfViewer();
}
});
</script>
</body>
</html>
`
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 `<div class="no-files">У вас пока нет загруженных документов</div>`
}
// Получаем токен для доступа к файлам
token, err := getFileToken(authCookie)
if err != nil {
log.Printf("Ошибка при получении токена файла: %v", err)
return `<div class="error">Ошибка при получении доступа к файлам</div>`
}
var html string
for _, fileName := range files {
// Формируем URL для просмотра файла с токеном
fileUrl := config.PocketBaseURL + "/api/files/_pb_users_auth_/" + userId + "/" + fileName + "?token=" + token
html += `
<div class="file-card" onclick="openPdfViewer('` + fileUrl + `')">
<div class="file-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
</div>
<div class="file-info">
<div class="file-name">` + fileName + `</div>
</div>
</div>
`
}
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 "Подтвержден"

View File

@ -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)

View File

@ -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)

View File

@ -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)

171
handlers/upload.go Normal file
View File

@ -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)
}

19
models/files.go Normal file
View File

@ -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"`
}

View File

@ -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"`
}
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"`
}

Binary file not shown.

Binary file not shown.

View File

@ -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=="}

View File

@ -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;
}
</style>
</head>
<body>
@ -78,6 +91,9 @@
<button type="submit">войти</button>
</form>
<div class="register-link">
<p>Нет аккаунта? <a href="/register">Зарегистрироваться</a></p>
</div>
</div>
</body>