diff --git a/API_DOCS.md b/API_DOCS.md deleted file mode 100644 index e7a346d..0000000 --- a/API_DOCS.md +++ /dev/null @@ -1,188 +0,0 @@ -## Calendar API Documentation - -### Аутентификация -Все эндпоинты требуют Basic авторизации. Используйте ваши учетные данные от CalDAV сервера. - -```bash -# Заголовки для всех запросов -Authorization: Basic base64(username:password) -CAL_ID: your_calendar_id -``` - -### Обязательные заголовки -- `Authorization`: Basic авторизация с креденшлами CalDAV -- `CAL_ID`: Идентификатор календаря (например: "personal", "work", "family") - -### Эндпоинты - -#### 1. Получить все события -```bash -GET /calendar/events -Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= -CAL_ID: personal - -# Ответ: -{ - "success": true, - "data": [ - { - "uid": "12345-67890-abcdef", - "summary": "Важная встреча", - "description": "Обсуждение проекта", - "startDateTime": "2025-07-20T10:00:00", - "endDateTime": "2025-07-20T11:30:00", - "location": "Конференц-зал" - } - ] -} -``` - -#### 2. Создать новое событие -```bash -POST /calendar/events -Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= -CAL_ID: personal -Content-Type: application/json - -{ - "summary": "Новая встреча", - "description": "Описание встречи", - "startDateTime": "2025-07-20T14:00:00", - "endDateTime": "2025-07-20T15:00:00", - "location": "Офис" -} - -# Ответ (201 Created): -{ - "success": true, - "data": { - "uid": "generated-uid-12345", - "summary": "Новая встреча", - "description": "Описание встречи", - "startDateTime": "2025-07-20T14:00:00", - "endDateTime": "2025-07-20T15:00:00", - "location": "Офис" - } -} -``` - -#### 3. Получить событие по UID -```bash -GET /calendar/events/{uid} -Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= -CAL_ID: personal - -# Ответ: -{ - "success": true, - "data": { - "uid": "12345-67890-abcdef", - "summary": "Важная встреча", - "description": "Обсуждение проекта", - "startDateTime": "2025-07-20T10:00:00", - "endDateTime": "2025-07-20T11:30:00", - "location": "Конференц-зал" - } -} -``` - -#### 4. Обновить событие -```bash -PUT /calendar/events/{uid} -Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= -CAL_ID: personal -Content-Type: application/json - -{ - "summary": "Обновленная встреча", - "description": "Новое описание", - "startDateTime": "2025-07-20T15:00:00", - "endDateTime": "2025-07-20T16:00:00", - "location": "Новый офис" -} - -# Ответ: -{ - "success": true, - "data": { - "uid": "12345-67890-abcdef", - "summary": "Обновленная встреча", - "description": "Новое описание", - "startDateTime": "2025-07-20T15:00:00", - "endDateTime": "2025-07-20T16:00:00", - "location": "Новый офис" - } -} -``` - -#### 5. Удалить событие -```bash -DELETE /calendar/events/{uid} -Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= -CAL_ID: personal - -# Ответ: -{ - "success": true, - "message": "Событие успешно удалено" -} -``` - -### Примеры с curl - -```bash -# Получить все события из календаря "work" -curl -X GET "http://localhost:8080/calendar/events" \ - -H "Authorization: Basic $(echo -n 'username:password' | base64)" \ - -H "CAL_ID: work" - -# Создать событие в календаре "personal" -curl -X POST "http://localhost:8080/calendar/events" \ - -H "Authorization: Basic $(echo -n 'username:password' | base64)" \ - -H "CAL_ID: personal" \ - -H "Content-Type: application/json" \ - -d '{ - "summary": "Тестовая встреча", - "description": "Описание тестовой встречи", - "startDateTime": "2025-07-20T10:00:00", - "endDateTime": "2025-07-20T11:00:00", - "location": "Онлайн" - }' - -# Обновить событие в календаре "family" -curl -X PUT "http://localhost:8080/calendar/events/your-event-uid" \ - -H "Authorization: Basic $(echo -n 'username:password' | base64)" \ - -H "CAL_ID: family" \ - -H "Content-Type: application/json" \ - -d '{ - "summary": "Семейный обед", - "startDateTime": "2025-07-20T14:00:00", - "endDateTime": "2025-07-20T15:00:00" - }' - -# Удалить событие из календаря "personal" -curl -X DELETE "http://localhost:8080/calendar/events/your-event-uid" \ - -H "Authorization: Basic $(echo -n 'username:password' | base64)" \ - -H "CAL_ID: personal" -``` - -### Ошибки - -При отсутствии заголовка `CAL_ID`: -```json -{ - "success": false, - "message": "Заголовок CAL_ID обязателен" -} -``` - -### Форматы дат -Все даты должны быть в формате ISO LocalDateTime: `YYYY-MM-DDTHH:mm:ss` -Например: `2025-07-20T14:30:00` - -### Поддерживаемые календари -API поддерживает работу с любыми календарями, доступными пользователю в CalDAV: -- `personal` - личный календарь -- `work` - рабочий календарь -- `family` - семейный календарь -- Любые другие календари, созданные пользователем diff --git a/build.gradle.kts b/build.gradle.kts index b070f24..5979f7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,3 @@ -val kotlin_version: String by project -val logback_version: String by project - plugins { kotlin("jvm") version "2.1.10" id("io.ktor.plugin") version "3.2.2" @@ -8,7 +5,7 @@ plugins { } group = "com.nano" -version = "0.0.1" +version = "0.0.2" application { mainClass = "com.nano.ApplicationKt" @@ -21,7 +18,6 @@ repositories { allprojects { repositories { - } } @@ -32,9 +28,10 @@ dependencies { implementation("io.ktor:ktor-server-content-negotiation") implementation("io.ktor:ktor-serialization-kotlinx-json") implementation("io.ktor:ktor-server-netty") - implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("ch.qos.logback:logback-classic:1.5.13") implementation("com.github.bitfireAT:dav4jvm:2.2.1") + implementation("io.ktor:ktor-client-logging:3.2.2") testImplementation("io.ktor:ktor-server-test-host") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.1.10") } diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..baa4532 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,621 @@ +openapi: "3.1.0" +info: + title: "CalDav-simple-RESTAPI" + description: "Примитивный REST API над CalDav потому что он старый и неудобный, а fancy cool modern REST API лучше" + version: "1.0.0" +servers: + - url: "http://localhost:8080" + description: "Local" +paths: + /calendar/{calendarId}/events: + get: + description: "Получение всех событий" + parameters: + - name: "Authorization" + in: "header" + required: false + schema: + type: "string" + - name: "calendarId" + in: "path" + required: true + schema: + type: "string" + responses: + "400": + description: "Bad Request" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "Заголовок CAL_ID обязателен" + "200": + description: "OK" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_List" + examples: + Example#1: + description: "" + value: + success: true + data: "null" + message: "null" + "500": + description: "Internal Server Error" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + Example#2: + description: "" + value: + success: false + data: "null" + message: "null" + post: + description: "Создание нового события" + parameters: + - name: "Authorization" + in: "header" + required: false + schema: + type: "string" + - name: "calendarId" + in: "path" + required: true + schema: + type: "string" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateEventRequest" + required: true + responses: + "400": + description: "Bad Request" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + Example#2: + description: "" + value: + success: false + data: "null" + message: "Заголовок CAL_ID обязателен" + "201": + description: "Created" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_EventResponse" + examples: + Example#1: + description: "" + value: + success: true + data: "null" + message: "null" + "401": + description: "Unauthorized" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "403": + description: "Forbidden" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "404": + description: "Not Found" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "409": + description: "Conflict" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "500": + description: "Internal Server Error" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + /calendar/{calendarId}/events/{uid}: + delete: + description: "Удаление события" + parameters: + - name: "uid" + in: "path" + required: true + schema: + type: "string" + - name: "Authorization" + in: "header" + required: false + schema: + type: "string" + - name: "calendarId" + in: "path" + required: true + schema: + type: "string" + responses: + "400": + description: "Bad Request" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + Example#2: + description: "" + value: + success: false + data: "null" + message: "Заголовок CAL_ID обязателен" + Example#3: + description: "" + value: + success: false + data: "null" + message: "UID события не указан" + "200": + description: "OK" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: true + data: "null" + message: "Событие успешно удалено" + "401": + description: "Unauthorized" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "403": + description: "Forbidden" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "404": + description: "Not Found" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "500": + description: "Internal Server Error" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + get: + description: "Получение события по UID" + parameters: + - name: "uid" + in: "path" + required: true + schema: + type: "string" + - name: "Authorization" + in: "header" + required: false + schema: + type: "string" + - name: "calendarId" + in: "path" + required: true + schema: + type: "string" + responses: + "400": + description: "Bad Request" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "Заголовок CAL_ID обязателен" + Example#2: + description: "" + value: + success: false + data: "null" + message: "UID события не указан" + "200": + description: "OK" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_EventResponse" + examples: + Example#1: + description: "" + value: + success: true + data: "null" + message: "null" + "404": + description: "Not Found" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + Example#2: + description: "" + value: + success: false + data: "null" + message: "Событие не найдено" + "401": + description: "Unauthorized" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "403": + description: "Forbidden" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "500": + description: "Internal Server Error" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + Example#2: + description: "" + value: + success: false + data: "null" + message: "null" + put: + description: "Обновление события" + parameters: + - name: "uid" + in: "path" + required: true + schema: + type: "string" + - name: "Authorization" + in: "header" + required: false + schema: + type: "string" + - name: "calendarId" + in: "path" + required: true + schema: + type: "string" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateEventRequest" + required: true + responses: + "400": + description: "Bad Request" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + Example#2: + description: "" + value: + success: false + data: "null" + message: "Заголовок CAL_ID обязателен" + Example#3: + description: "" + value: + success: false + data: "null" + message: "UID события не указан" + "200": + description: "OK" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_EventResponse" + examples: + Example#1: + description: "" + value: + success: true + data: "null" + message: "null" + "401": + description: "Unauthorized" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "403": + description: "Forbidden" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "404": + description: "Not Found" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "412": + description: "Precondition Failed" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" + "500": + description: "Internal Server Error" + content: + '*/*': + schema: + $ref: "#/components/schemas/ApiResponse_String" + examples: + Example#1: + description: "" + value: + success: false + data: "null" + message: "null" +components: + schemas: + ApiResponse: + type: "object" + properties: + success: + type: "boolean" + message: + type: "string" + nullable: true + required: + - "success" + ApiResponse_String: + type: "object" + properties: + success: + type: "boolean" + data: + type: "string" + message: + type: "string" + EventResponse: + type: "object" + properties: + uid: + type: "string" + summary: + type: "string" + description: + type: "string" + nullable: true + startDateTime: + type: "string" + endDateTime: + type: "string" + location: + type: "string" + nullable: true + required: + - "uid" + - "summary" + - "startDateTime" + - "endDateTime" + ApiResponse_List: + type: "object" + properties: + success: + type: "boolean" + data: + type: "array" + items: + $ref: "#/components/schemas/EventResponse" + message: + type: "string" + CreateEventRequest: + type: "object" + properties: + summary: + type: "string" + description: + type: "string" + nullable: true + startDateTime: + type: "string" + endDateTime: + type: "string" + location: + type: "string" + nullable: true + required: + - "summary" + - "startDateTime" + - "endDateTime" + ApiResponse_EventResponse: + type: "object" + properties: + success: + type: "boolean" + data: + $ref: "#/components/schemas/EventResponse" + message: + type: "string" \ No newline at end of file diff --git a/src/main/kotlin/Routing.kt b/src/main/kotlin/Routing.kt index 1b9f549..478274a 100644 --- a/src/main/kotlin/Routing.kt +++ b/src/main/kotlin/Routing.kt @@ -44,7 +44,7 @@ data class ApiResponse( fun Application.configureRouting() { routing { authenticate("calendar-auth") { - route("/calendar") { + route("/calendar/{calendarId}") { route("/events") { // Получение всех событий @@ -62,9 +62,9 @@ fun Application.configureRouting() { } ?: "" // Получаем календарь ID из заголовка CAL_ID - val calendarId = call.request.headers["CAL_ID"] ?: return@get call.respond( + val calendarId = call.parameters["calendarId"] ?: return@get call.respond( HttpStatusCode.BadRequest, - ApiResponse(success = false, message = "Заголовок CAL_ID обязателен") + ApiResponse(success = false, message = "Не найден параметр calendarId") ) val dav = Dav(username, password, calendarId) @@ -113,9 +113,9 @@ fun Application.configureRouting() { } ?: "" // Получаем календарь ID из заголовка CAL_ID - val calendarId = call.request.headers["CAL_ID"] ?: return@post call.respond( + val calendarId = call.parameters["calendarId"] ?: return@post call.respond( HttpStatusCode.BadRequest, - ApiResponse(success = false, message = "Заголовок CAL_ID обязателен") + ApiResponse(success = false, message = "Не найден параметр calendarId") ) val request = call.receive() @@ -180,9 +180,9 @@ fun Application.configureRouting() { } ?: "" // Получаем календарь ID из заголовка CAL_ID - val calendarId = call.request.headers["CAL_ID"] ?: return@get call.respond( + val calendarId = call.parameters["calendarId"] ?: return@get call.respond( HttpStatusCode.BadRequest, - ApiResponse(success = false, message = "Заголовок CAL_ID обязателен") + ApiResponse(success = false, message = "Не найден параметр calendarId") ) val dav = Dav(username, password, calendarId) @@ -243,9 +243,9 @@ fun Application.configureRouting() { } ?: "" // Получаем календарь ID из заголовка CAL_ID - val calendarId = call.request.headers["CAL_ID"] ?: return@put call.respond( + val calendarId = call.parameters["calendarId"] ?: return@put call.respond( HttpStatusCode.BadRequest, - ApiResponse(success = false, message = "Заголовок CAL_ID обязателен") + ApiResponse(success = false, message = "Не найден параметр calendarId") ) val request = call.receive() @@ -311,9 +311,9 @@ fun Application.configureRouting() { } ?: "" // Получаем календарь ID из заголовка CAL_ID - val calendarId = call.request.headers["CAL_ID"] ?: return@delete call.respond( + val calendarId = call.parameters["calendarId"] ?: return@delete call.respond( HttpStatusCode.BadRequest, - ApiResponse(success = false, message = "Заголовок CAL_ID обязателен") + ApiResponse(success = false, message = "Не найден параметр calendarId") ) val dav = Dav(username, password, calendarId) diff --git a/src/main/kotlin/Serialization.kt b/src/main/kotlin/Serialization.kt index 942c72e..45a393b 100644 --- a/src/main/kotlin/Serialization.kt +++ b/src/main/kotlin/Serialization.kt @@ -14,9 +14,4 @@ fun Application.configureSerialization() { install(ContentNegotiation) { json() } - routing { - get("/json/kotlinx-serialization") { - call.respond(mapOf("hello" to "world")) - } - } }