diff --git a/Dockerfile b/Dockerfile index 06a0905..49151ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,22 @@ -# syntax=docker/dockerfile:1.5 +# Этап сборки +FROM golang:1.21-alpine AS builder -# Build stage -ARG GO_VERSION=1.24 -FROM golang:${GO_VERSION} AS builder - -# Set the working directory +# Устанавливаем рабочую директорию WORKDIR /app -# Cache dependencies and build the application -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=bind,target=. \ - CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /task-manager ./cmd/main.go +# Копируем файлы проекта +COPY go.mod go.sum ./ +RUN go mod download -# Final stage +COPY . ./ + +# Сборка бинарного файла +RUN go build -o /task_manager ./cmd/task_manager + +# Финальный минимальный образ FROM scratch AS final -# Set the working directory WORKDIR /app - -# Copy the built binary from the builder stage -COPY --from=builder /task-manager ./ - -# Expose the application port +COPY --from=builder /task_manager . EXPOSE 8080 - -# Set the default command to run the application -CMD ["./task-manager"] \ No newline at end of file +CMD ["./task_manager"] \ No newline at end of file diff --git a/cmd/main.go b/cmd/task_manager/main.go similarity index 100% rename from cmd/main.go rename to cmd/task_manager/main.go diff --git a/compose.yaml b/compose.yaml index 34eda56..9574d7e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,4 +16,15 @@ services: - 5432:5432 volumes: - ./database/init:/docker-entrypoint-initdb.d + restart: unless-stopped + + swagger-ui: + image: swaggerapi/swagger-ui:latest + container_name: swagger-ui + ports: + - "8081:8080" + volumes: + - ./config/swagger.yaml:/tmp/swagger.yaml:ro + environment: + - SWAGGER_JSON=/tmp/swagger.yaml restart: unless-stopped \ No newline at end of file diff --git a/config/swagger.yaml b/config/swagger.yaml new file mode 100644 index 0000000..a657ae2 --- /dev/null +++ b/config/swagger.yaml @@ -0,0 +1,254 @@ +openapi: "3.0.0" +info: + title: "Task Manager API" + description: "API для управления задачами, проектами и пользователями" + version: "1.0.0" + +servers: + - url: "http://localhost:8080/api/v1" + description: "Локальный сервер для тестирования API" + +paths: + /user/login: + post: + summary: "Вход пользователя в систему" + description: "Авторизация пользователя по имени и паролю" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "johndoe" + description: "Имя пользователя" + password: + type: string + example: "12345" + description: "Пароль пользователя" + responses: + "200": + description: "Авторизация успешна" + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: string + "401": + description: "Недействительный логин или пароль" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /user/register: + post: + summary: "Регистрация нового пользователя" + description: "Создание нового аккаунта пользователя" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/User" + responses: + "201": + description: "Пользователь успешно зарегистрирован" + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "400": + description: "Ошибка в запросе" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /user/tasks: + get: + summary: "Получить задачи пользователя" + description: "Возвращает список задач для указанного пользователя" + parameters: + - name: "id_user" + in: query + required: true + schema: + type: integer + example: 1 + description: "ID пользователя" + responses: + "200": + description: "Список задач пользователя" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Task" + "400": + description: "Отсутствует ID пользователя" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /user/projects: + get: + summary: "Получить проекты пользователя" + description: "Возвращает список проектов, связанных с пользователем" + parameters: + - name: "id_user" + in: query + required: true + schema: + type: integer + example: 1 + description: "ID пользователя" + responses: + "200": + description: "Список проектов" + content: + application/json: + schema: + type: array + items: + type: string + "400": + description: "Отсутствует ID пользователя" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /projects: + post: + summary: "Создать проект" + description: "Создание нового проекта (в разработке)" + responses: + "501": + description: "Не реализовано" + content: + application/json: + schema: + $ref: "#/components/schemas/UnimplementedResponse" + get: + summary: "Получить проекты" + description: "Получение списка всех проектов (в разработке)" + responses: + "501": + description: "Не реализовано" + content: + application/json: + schema: + $ref: "#/components/schemas/UnimplementedResponse" + /projects/tasks: + post: + summary: "Создать задачу" + description: "Создание новой задачи для проекта" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Task" + responses: + "201": + description: "Задача успешно создана" + content: + application/json: + schema: + $ref: "#/components/schemas/Task" + "400": + description: "Ошибка в запросе" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /projects/tasks/{task_id}: + patch: + summary: "Обновить задачу" + description: "Изменение данных задачи по ID" + parameters: + - name: "task_id" + in: path + required: true + schema: + type: integer + example: 42 + description: "ID задачи" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Task" + responses: + "200": + description: "Задача успешно обновлена" + content: + application/json: + schema: + $ref: "#/components/schemas/Task" + "400": + description: "Ошибка в запросе" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /projects/sprints: + post: + summary: "Создать спринт" + description: "Создание спринта в проекте (в разработке)" + responses: + "501": + description: "Не реализовано" + content: + application/json: + schema: + $ref: "#/components/schemas/UnimplementedResponse" + +components: + schemas: + ErrorResponse: + type: object + properties: + code: + type: integer + description: "HTTP-код ошибки" + message: + type: string + description: "Сообщение об ошибке" + error: + type: string + description: "Машинное описание ошибки" + UnimplementedResponse: + type: object + properties: + message: + type: string + example: "Not implemented" + User: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: string + password: + type: string + Task: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string \ No newline at end of file diff --git a/internal/app/handlers.go b/internal/app/handlers.go index fcf672f..d1d6eec 100644 --- a/internal/app/handlers.go +++ b/internal/app/handlers.go @@ -1,7 +1,13 @@ package app import ( + "encoding/json" + "errors" "net/http" + "strconv" + "task_manager/internal/domain/tasks" + "task_manager/internal/domain/users" + "task_manager/internal/persistance" gorilla "github.com/gorilla/mux" ) @@ -35,16 +41,174 @@ func (rm *TaskManager) RegisterRoutes(router *gorilla.Router) { } -func (rm *TaskManager) handleLogin(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleRegistrateUser(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleUserTasks(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleUserProjects(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleAssignUserToProject(w http.ResponseWriter, r *http.Request) {} +func (rm *TaskManager) handleLogin(w http.ResponseWriter, r *http.Request) { + var input struct { + Name string `json:"name"` + Password string `json:"password"` + } -func (rm *TaskManager) handleCreateProject(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleCreateTask(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleCreateSprint(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleGetProjectUsers(w http.ResponseWriter, r *http.Request) {} + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload", err) + return + } -func (rm *TaskManager) handleUpdateTask(w http.ResponseWriter, r *http.Request) {} -func (rm *TaskManager) handleGetProjects(w http.ResponseWriter, r *http.Request) {} + user, err := rm.repo.GetUser(input.Name) + if err != nil || user.Password != persistance.GetMD5Hash(input.Password) { + respondWithError(w, http.StatusUnauthorized, "Invalid username or password", errors.New("authentication failed")) + return + } + + respondWithJSON(w, http.StatusOK, user) +} +func (rm *TaskManager) handleRegistrateUser(w http.ResponseWriter, r *http.Request) { + var user users.User + err := json.NewDecoder(r.Body).Decode(&user) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload", err) + return + } + + err = rm.repo.AddUser(&user) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to create user", err) + return + } + + respondWithJSON(w, http.StatusCreated, user) +} +func (rm *TaskManager) handleUserTasks(w http.ResponseWriter, r *http.Request) { + userID := r.URL.Query().Get("id_user") + if userID == "" { + respondWithError(w, http.StatusBadRequest, "Missing user ID", errors.New("id_user parameter required")) + return + } + + id, err := strconv.Atoi(userID) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid user ID", err) + return + } + + tasks, err := rm.repo.GetUserTasks(id) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to fetch user tasks", err) + return + } + + respondWithJSON(w, http.StatusOK, tasks) +} +func (rm *TaskManager) handleUserProjects(w http.ResponseWriter, r *http.Request) { + userID := r.URL.Query().Get("id_user") + if userID == "" { + respondWithError(w, http.StatusBadRequest, "Missing user ID", errors.New("id_user parameter required")) + return + } + + _, err := strconv.Atoi(userID) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid user ID", err) + return + } + + // Заглушка: здесь предполагается метод получения проектов пользователя + // Для реализации необходимо будет добавить соответствующий метод в репозиторий + projects := []string{"Project 1", "Project 2"} // Пример данных + + respondWithJSON(w, http.StatusOK, projects) +} +func (rm *TaskManager) handleAssignUserToProject(w http.ResponseWriter, r *http.Request) { + var input struct { + IdUser int `json:"id_user"` + IdProject int `json:"id_project"` + } + + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload", err) + return + } + + // Заглушка: реализация привязки пользователя к проекту + // Для добавления необходимо доработать метод в репозитории + + respondWithJSON(w, http.StatusOK, map[string]string{ + "message": "User assigned to project successfully", + }) +} + +func (rm *TaskManager) handleCreateProject(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusNotImplemented, map[string]string{ + "message": "Create project not implemented yet", + }) +} +func (rm *TaskManager) handleCreateTask(w http.ResponseWriter, r *http.Request) { + var task tasks.Task + err := json.NewDecoder(r.Body).Decode(&task) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload", err) + return + } + + err = rm.repo.AddTask(task) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to create task", err) + return + } + + respondWithJSON(w, http.StatusCreated, task) +} +func (rm *TaskManager) handleCreateSprint(w http.ResponseWriter, r *http.Request) { + taskID := gorilla.Vars(r)["task_id"] + id, err := strconv.Atoi(taskID) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid task ID", err) + return + } + + var task tasks.Task + err = json.NewDecoder(r.Body).Decode(&task) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload", err) + return + } + + task.Id = id + err = rm.repo.UpdateTask(task) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to update task", err) + return + } + + respondWithJSON(w, http.StatusOK, task) +} +func (rm *TaskManager) handleGetProjectUsers(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusNotImplemented, map[string]string{ + "message": "Get projects not implemented yet", + }) +} + +func (rm *TaskManager) handleUpdateTask(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusNotImplemented, map[string]string{ + "message": "Create sprint not implemented yet", + }) +} +func (rm *TaskManager) handleGetProjects(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusNotImplemented, map[string]string{ + "message": "Get project users not implemented yet", + }) +} + +func respondWithError(w http.ResponseWriter, code int, message string, err error) { + w.WriteHeader(code) + json.NewEncoder(w).Encode(ErrorResponse{ + Code: code, + Message: message, + Error: err.Error(), + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.WriteHeader(code) + json.NewEncoder(w).Encode(payload) +}