Документ описывает текущий backend API и то, как подключать его в Android-приложении.
Локально:
http://10.0.2.2:8080
10.0.2.2 используется Android Emulator для доступа к localhost хост-машины.
Для реального устройства в одной Wi-Fi сети:
http://<LAN_IP_компьютера>:8080
В production должен быть HTTPS:
https://api.example.com
- JSON API использует
Content-Type: application/json. - Защищенные запросы требуют заголовок:
Authorization: Bearer <accessToken>accessToken- JWT access token.refreshToken- opaque token, используется только для/auth/refresh.currentUserIdна backend берется изsubвнутри Bearer JWT.- ID в некоторых DTO сейчас смешанные: профиль отдается как строка
user-1, контакты/сообщения какLong. - Время отдается строкой ISO-8601.
Ошибки приходят единым JSON:
{
"code": "CONTACT_NOT_FOUND",
"message": "Contact not found"
}Клиенту стоит обрабатывать минимум:
| HTTP | Пример code | Что делать |
|---|---|---|
| 400 | INVALID_OTP, INVALID_CONTACT_ID |
Показать ошибку формы/запроса |
| 401 | UNAUTHORIZED, INVALID_CREDENTIALS, INVALID_REFRESH_TOKEN |
Refresh token или logout |
| 404 | CONTACT_NOT_FOUND, CHAT_NOT_FOUND, MESSAGE_NOT_FOUND |
Показать empty/error state |
| 409 | EMAIL_ALREADY_REGISTERED |
Показать ошибку регистрации |
| 415 | UNSUPPORTED_AVATAR_TYPE |
Попросить выбрать PNG |
| 500 | INTERNAL_SERVER_ERROR |
Показать общую ошибку |
Регистрация:
- Android вызывает
POST /auth/register. - Backend создает пользователя
isVerified=false. - Backend генерирует OTP и кладет задачу в
otp_outbox. runOtpWorkerотправляет код с системной почты.- Android показывает экран ввода кода.
- Android вызывает
POST /auth/verify-otp. - Backend возвращает
accessToken,refreshToken,profile.
Вход:
- Android вызывает
POST /auth/login. - Backend проверяет email/password и
isVerified=true. - Backend возвращает
accessToken,refreshToken,profile.
OTP используется только при регистрации. Обычный вход выполняется по email и паролю.
Auth: нет.
Request:
{
"email": "user@mail.com",
"password": "password123",
"displayName": "Ivan"
}Response 201 Created:
{
"message": "OTP code sent"
}Ошибки:
{
"code": "EMAIL_ALREADY_REGISTERED",
"message": "Email already registered"
}Auth: нет.
Request:
{
"email": "user@mail.com",
"code": "123456"
}Response 200 OK:
{
"accessToken": "jwt-access-token",
"refreshToken": "refresh-token",
"profile": {
"id": "user-1",
"name": "Ivan",
"username": "@user1",
"email": "user@mail.com",
"birthday": null,
"bio": null,
"avatarUrl": "/media/avatars/profile-1-avatar.png",
"isOnline": false
}
}Ошибка:
{
"code": "INVALID_OTP",
"message": "Invalid OTP"
}Auth: нет.
Request:
{
"email": "user@mail.com",
"password": "password123"
}Response такой же, как у /auth/verify-otp.
Ошибка:
{
"code": "INVALID_CREDENTIALS",
"message": "Invalid credentials or email is not verified"
}Auth: нет.
Request:
{
"refreshToken": "refresh-token"
}Response:
{
"accessToken": "new-jwt-access-token",
"refreshToken": "new-refresh-token",
"profile": {
"id": "user-1",
"name": "Ivan",
"username": "@user1",
"email": "user@mail.com",
"birthday": null,
"bio": null,
"avatarUrl": "/media/avatars/profile-1-avatar.png",
"isOnline": false
}
}Важно: backend ротирует refresh token. Android должен заменить оба токена после успешного refresh.
Auth: Bearer access token.
Request body: пустой.
Response:
204 No ContentBackend очищает refresh token hash для текущего пользователя.
Все profile endpoints требуют Bearer token.
Response:
{
"id": "user-1",
"name": "Ivan",
"username": "@user1",
"email": "user@mail.com",
"birthday": "2000-01-01",
"bio": "Hello",
"avatarUrl": "/media/avatars/profile-1-avatar.png",
"isOnline": true
}Request, все поля optional:
{
"name": "Ivan Ivanov",
"username": "@ivan",
"bio": "Android developer",
"birthday": "2000-01-01"
}Response: ProfileResponse.
userId можно передавать как 1 или user-1.
Response: ProfileResponse.
Auth: Bearer access token.
Request:
multipart/form-data
field name: avatar
content type: image/png
Response:
{
"avatarUrl": "/media/avatars/profile-1-avatar.png"
}Ошибки:
{
"code": "AVATAR_CONTENT_TYPE_REQUIRED",
"message": "Avatar content type is required"
}{
"code": "UNSUPPORTED_AVATAR_TYPE",
"message": "Avatar must be image/png"
}Все settings endpoints требуют Bearer token.
Response:
{
"language": "ru",
"hasMailAccessToken": true,
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"imapHost": "imap.example.com",
"imapPort": 993
}Request:
{
"language": "en"
}Response: SettingsResponse.
Request:
{
"token": "provider-access-token-or-app-password",
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"imapHost": "imap.example.com",
"imapPort": 993
}smtpHost, smtpPort, imapHost, imapPort optional. Если они не переданы, worker попробует определить настройки по email или взять defaults из application.yaml.
Response:
204 No ContentResponse:
204 No ContentResponse:
{
"configured": true
}Все contacts endpoints требуют Bearer token.
Query:
q optional
Response:
{
"items": [
{
"id": 1,
"email": "friend@mail.com",
"displayName": "Friend",
"avatarUrl": null,
"createdAt": "2026-05-27T12:00:00Z",
"updatedAt": "2026-05-27T12:00:00Z"
}
]
}Response: ContactResponse.
Request:
{
"email": "friend@mail.com",
"displayName": "Friend"
}Response 201 Created: ContactResponse.
Request:
{
"displayName": "Best Friend",
"avatarUrl": "/media/avatars/friend.png"
}Response: ContactResponse.
Response:
204 No ContentСоздает или возвращает существующий чат с контактом.
Response:
{
"id": "1",
"title": "Friend",
"initials": "F",
"lastMessage": null,
"lastMessageTime": null,
"unreadCount": 0,
"isOnline": false
}Все message endpoints требуют Bearer token.
Query:
limit optional, default 50, range 1..100
offset optional, default 0
Response:
{
"items": [
{
"id": 1,
"chatId": 1,
"bodyText": "Hello",
"direction": "OUTGOING",
"status": "SENT",
"isRead": false,
"createdAt": "2026-05-27T12:00:00Z",
"sentAt": "2026-05-27T12:00:01Z",
"receivedAt": null
}
]
}Request:
{
"bodyText": "Hello"
}Response 201 Created:
{
"message": {
"id": 1,
"chatId": 1,
"bodyText": "Hello",
"direction": "OUTGOING",
"status": "PENDING",
"isRead": false,
"createdAt": "2026-05-27T12:00:00Z",
"sentAt": null,
"receivedAt": null
}
}После создания backend кладет письмо в mail_outbox, а runMailWorker отправляет его через SMTP пользователя.
Response: MessageResponse.
Response:
204 No ContentПометить сообщения чата прочитанными.
Response:
204 No ContentResponse:
204 No ContentФайлы доступны через:
/media/**
Например:
http://10.0.2.2:8080/media/avatars/profile-1-avatar.png
Если avatarUrl приходит относительным путем, Android должен добавить baseUrl.
Текущий endpoint:
ws://10.0.2.2:8080/ws
Сейчас backend websocket реализован как echo endpoint:
client: hello
server: YOU SAID: hello
Полноценные auth-события чатов пока не реализованы. Для Android MVP лучше использовать REST polling/refresh списка сообщений. Когда WebSocket будет расширяться, нужно добавить Bearer JWT-auth на handshake и события NEW_MESSAGE, MESSAGE_SENT, MESSAGE_READ, CHAT_UPDATED.
Рекомендуемые Kotlin DTO под текущий API:
@Serializable
data class RegisterRequestDto(
val email: String,
val password: String,
val displayName: String? = null
)
@Serializable
data class RegisterResponseDto(
val message: String
)
@Serializable
data class VerifyOtpRequestDto(
val email: String,
val code: String
)
@Serializable
data class LoginRequestDto(
val email: String,
val password: String
)
@Serializable
data class RefreshRequestDto(
val refreshToken: String
)
@Serializable
data class AuthResponseDto(
val accessToken: String,
val refreshToken: String,
val profile: ProfileDto
)
@Serializable
data class ProfileDto(
val id: String,
val name: String,
val username: String,
val email: String,
val birthday: String?,
val bio: String?,
val avatarUrl: String,
val isOnline: Boolean
)@Serializable
data class ErrorResponseDto(
val code: String,
val message: String
)Для enum:
enum class MessageDirection {
INCOMING,
OUTGOING
}
enum class MessageStatus {
PENDING,
SENT,
FAILED,
READ
}Если Android enum отличается, добавить fallback:
@Serializable
enum class MessageStatusDto {
PENDING,
SENT,
FAILED,
READ,
UNKNOWN
}Пример:
interface AuthApi {
@POST("auth/register")
suspend fun register(@Body body: RegisterRequestDto): RegisterResponseDto
@POST("auth/verify-otp")
suspend fun verifyOtp(@Body body: VerifyOtpRequestDto): AuthResponseDto
@POST("auth/login")
suspend fun login(@Body body: LoginRequestDto): AuthResponseDto
@POST("auth/refresh")
suspend fun refresh(@Body body: RefreshRequestDto): AuthResponseDto
@POST("auth/logout")
suspend fun logout(): Response<Unit>
}interface ProfileApi {
@GET("profile/me")
suspend fun getMe(): ProfileDto
@PATCH("profile/me")
suspend fun updateMe(@Body body: UpdateProfileRequestDto): ProfileDto
@Multipart
@POST("profile/avatar")
suspend fun uploadAvatar(@Part avatar: MultipartBody.Part): UploadProfileAvatarResponseDto
}interface ContactsApi {
@GET("contacts")
suspend fun getContacts(@Query("q") query: String? = null): ContactListResponseDto
@POST("contacts")
suspend fun createContact(@Body body: CreateContactRequestDto): ContactDto
@POST("contacts/{contactId}/chat")
suspend fun getOrCreateChat(@Path("contactId") contactId: Long): ChatDto
}interface MessagesApi {
@GET("chats/{chatId}/messages")
suspend fun getMessages(
@Path("chatId") chatId: Long,
@Query("limit") limit: Int = 50,
@Query("offset") offset: Long = 0
): MessageListResponseDto
@POST("chats/{chatId}/messages")
suspend fun sendMessage(
@Path("chatId") chatId: Long,
@Body body: SendMessageRequestDto
): SendMessageResponseDto
}Добавлять Bearer token ко всем запросам, кроме /auth/register, /auth/verify-otp, /auth/login, /auth/refresh.
class AuthInterceptor(
private val tokenStore: TokenStore
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val path = request.url.encodedPath
val isPublicAuth = path in setOf(
"/auth/register",
"/auth/verify-otp",
"/auth/login",
"/auth/refresh"
)
val accessToken = tokenStore.accessToken()
val authenticatedRequest = if (!isPublicAuth && !accessToken.isNullOrBlank()) {
request.newBuilder()
.header("Authorization", "Bearer $accessToken")
.build()
} else {
request
}
return chain.proceed(authenticatedRequest)
}
}Рекомендуемая логика:
- Если REST вернул
401, попробовать один раз вызвать/auth/refresh. - Если refresh успешен, сохранить новые
accessTokenиrefreshToken. - Повторить исходный запрос.
- Если refresh вернул
401, очистить сессию и открыть login screen.
Важно: не запускать много refresh-запросов параллельно. Использовать Mutex.
При старте приложения:
- Прочитать токены из encrypted storage.
- Если токенов нет - открыть auth screen.
- Если есть
accessToken, вызватьGET /profile/me. - Если
401, выполнить refresh. - После успешной авторизации загрузить:
GET /profile/meGET /settingsGET /contacts
Для регистрации OTP должен быть запущен:
.\gradlew.bat runOtpWorkerДля отправки пользовательских сообщений и IMAP sync:
.\gradlew.bat runMailWorkerSMTP нашей системной почты задается в src/main/resources/application.yaml:
mail:
auth:
fromEmail: "noreply@example.com"
username: "noreply@example.com"
accessToken: "smtp-access-token"
smtpHost: "smtp.example.com"
smtpPort: 587Токен доступа лучше передавать через env:
accessToken: "$POSTER_AUTH_SMTP_ACCESS_TOKEN:"Compose поднимает PostgreSQL, API и два фоновых процесса:
docker compose up --buildСервисы:
postgres- базаposterDbна порту5432.api- Ktor API на порту8080.mail-worker- отправка исходящих сообщений и IMAP sync.otp-worker- отправка OTP-кодов для регистрации.
Для OTP worker перед запуском задайте SMTP-переменные:
$env:POSTER_AUTH_SMTP_FROM = "noreply@example.com"
$env:POSTER_AUTH_SMTP_USERNAME = "noreply@example.com"
$env:POSTER_AUTH_SMTP_ACCESS_TOKEN = "smtp-access-token"
$env:POSTER_AUTH_SMTP_HOST = "smtp.example.com"
$env:POSTER_AUTH_SMTP_PORT = "587"- Android base URL для emulator:
http://10.0.2.2:8080/. - Реализовать auth screens: register, verify OTP, login.
- Сохранять оба токена после
verify-otp,login,refresh. - Добавить OkHttp interceptor для Bearer token.
- Добавить refresh-on-401.
- Подключить profile/settings/contacts/messages endpoints.
- Для avatar upload отправлять только PNG multipart field
avatar. - Не полагаться на WebSocket для MVP, он пока echo-only.
- На backend запустить API, PostgreSQL,
runOtpWorker; для сообщений такжеrunMailWorker.