Merge pull request #2

master
This commit is contained in:
2025-07-18 06:37:06 +06:00
committed by GitHub
5 changed files with 636 additions and 211 deletions

View File

@@ -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` - семейный календарь
- Любые другие календари, созданные пользователем

View File

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

621
openapi.yaml Normal file
View File

@@ -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<? extends EventResponse>"
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<? extends EventResponse>:
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"

View File

@@ -44,7 +44,7 @@ data class ApiResponse<T>(
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<String>(success = false, message = "Заголовок CAL_ID обязателен")
ApiResponse<String>(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<String>(success = false, message = "Заголовок CAL_ID обязателен")
ApiResponse<String>(success = false, message = "Не найден параметр calendarId")
)
val request = call.receive<CreateEventRequest>()
@@ -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<String>(success = false, message = "Заголовок CAL_ID обязателен")
ApiResponse<String>(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<String>(success = false, message = "Заголовок CAL_ID обязателен")
ApiResponse<String>(success = false, message = "Не найден параметр calendarId")
)
val request = call.receive<CreateEventRequest>()
@@ -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<String>(success = false, message = "Заголовок CAL_ID обязателен")
ApiResponse<String>(success = false, message = "Не найден параметр calendarId")
)
val dav = Dav(username, password, calendarId)

View File

@@ -14,9 +14,4 @@ fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
routing {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}