api endpoints fix and inclusion
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-08-10 15:39:48 +09:00
parent 7ecc556c77
commit b595bcc9bc
65 changed files with 6046 additions and 263 deletions

2
.gitignore vendored
View File

@@ -17,7 +17,7 @@ build/
# IDE / OS
.DS_Store
.idea/
.idea/a
.vscode/
# Docker

1
chat/src/app/api/chat.py Normal file
View File

@@ -0,0 +1 @@
'"$@"'

View File

@@ -0,0 +1,454 @@
[14:52:17] OpenAPI: http://localhost:8080/openapi.json
[14:52:17] ✔ OpenAPI fetched (32448 bytes)
[14:52:17] Prepare tokens
[14:52:17] REGISTER admin+20250810_145217@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
[14:52:17] TOKEN for admin+20250810_145217@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhY2Q5ZTQ5YS1jNTQ5LTQxZDUtODA0NC0zODZkODMzZmVlYjQiLCJlbWFpbCI6ImFkbWluKzIwMjUwODEwXzE0NTIxN0BhdWRpdC5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNjAzN30.dFVm89lOssfFuTI5MJWGTi4c6fVLc80sFZkbdg5o_SU","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhY2Q5ZTQ5YS1jNTQ5LTQxZDUtODA0NC0zODZkODMzZmVlYjQiLCJlbWFpbCI6ImFkbWluKzIwMjUwODEwXzE0NTIxN0BhdWRpdC5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6InJlZnJlc2giLCJ[14:52:18] REGISTER client+20250810_145217@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
[14:52:18] TOKEN for client+20250810_145217@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiYzU5YTRiOC03YzA5LTRkMGQtODE3NC1kYjM1ZmRhNzAxOWYiLCJlbWFpbCI6ImNsaWVudCsyMDI1MDgxMF8xNDUyMTdAYXVkaXQuZGV2Iiwicm9sZSI6IkNMSUVOVCIsInR5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NTQ4MDYwMzh9.JnJ-rl58Qtpf8PY-1OjhDjlrCJ66vR-nFO0tEQad7QM","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiYzU5YTRiOC03YzA5LTRkMGQtODE3NC1kYjM1ZmRhNzAxOWYiLCJlbWFpbCI6ImNsaWVudCsyMDI1MDgxMF8xNDUyMTdAYXVkaXQuZGV2Iiwicm9sZSI6IkNMSUVOVCIsInR5cGUiOiJyZWZyZXNoIi[14:52:18] ✔ Tokens acquired
[14:52:18] Analyze pathparameters in schema…
[14:52:18] ✖ Найдены операции без объявленных pathпараметров: 19
[
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
[14:52:18] SMOKE missing-param operations…

View File

@@ -0,0 +1,449 @@
{
"base_url": "http://localhost:8080",
"openapi_url": "http://localhost:8080/openapi.json",
"ts": "20250810_145252",
"summary": {
"missing_path_param_ops": 19
},
"findings": {
"missing_path_params": [
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
}
}

View File

@@ -0,0 +1,445 @@
[14:52:52] OpenAPI: http://localhost:8080/openapi.json
[14:52:52] ✔ OpenAPI fetched (32448 bytes)
[14:52:52] Analyze pathparameters in schema…
[14:52:52] ✖ Найдены операции без объявленных pathпараметров: 19
[
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
[14:52:52] Report: logs/audit_20250810_145252.json
[14:52:52] Log: logs/audit_20250810_145252.log

View File

@@ -0,0 +1,449 @@
{
"base_url": "http://localhost:8080",
"openapi_url": "http://localhost:8080/openapi.json",
"ts": "20250810_145805",
"summary": {
"missing_path_param_ops": 19
},
"findings": {
"missing_path_params": [
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
}
}

View File

@@ -0,0 +1,494 @@
[14:58:05] OpenAPI: http://localhost:8080/openapi.json
[14:58:05] ✔ OpenAPI fetched (32448 bytes)
[14:58:05] Prepare tokens
[14:58:05] REGISTER admin+20250810_145805@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
[14:58:05] TOKEN for admin+20250810_145805@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NWEzMDU2Mi0yZWZkLTQwODgtODljNi1mZDViYTE2NzlhMGEiLCJlbWFpbCI6ImFkbWluKzIwMjUwODEwXzE0NTgwNUBhdWRpdC5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNjM4NX0.jpBrPjuIurUlY2FzmOjOeiH82Y0OuujKxK10uykHPWc","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NWEzMDU2Mi0yZWZkLTQwODgtODljNi1mZDViYTE2NzlhMGEiLCJlbWFpbCI6ImFkbWluKzIwMjUwODEwXzE0NTgwNUBhdWRpdC5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6InJlZnJlc2giLCJ[14:58:05] REGISTER client+20250810_145805@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
[14:58:06] TOKEN for client+20250810_145805@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODA2YWExMS0xYzQ0LTRjZWMtYjA1Ni1kOWViMjk2ZjNiN2MiLCJlbWFpbCI6ImNsaWVudCsyMDI1MDgxMF8xNDU4MDVAYXVkaXQuZGV2Iiwicm9sZSI6IkNMSUVOVCIsInR5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NTQ4MDYzODZ9.LIs31GuKJTlwWO9L5jCWj6CgQ_cX9go4r4WCtKC5LDc","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODA2YWExMS0xYzQ0LTRjZWMtYjA1Ni1kOWViMjk2ZjNiN2MiLCJlbWFpbCI6ImNsaWVudCsyMDI1MDgxMF8xNDU4MDVAYXVkaXQuZGV2Iiwicm9sZSI6IkNMSUVOVCIsInR5cGUiOiJyZWZyZXNoIi[14:58:06] ✔ Tokens acquired
[14:58:06] Analyze pathparameters in schema…
[14:58:06] ✖ Найдены операции без объявленных pathпараметров: 19
[
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
[14:58:06] SMOKE missing-param operations…
[14:58:06] → get http://localhost:8080/auth/v1/users/{user_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → delete http://localhost:8080/auth/v1/users/{user_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → patch http://localhost:8080/auth/v1/users/{user_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → get http://localhost:8080/profiles/v1/profiles/{profile_id} (security: [{HTTPBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → get http://localhost:8080/profiles/v1/profiles/by-user/{user_id} (security: [{HTTPBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → put http://localhost:8080/profiles/v1/likes/{target_user_id} (security: [{HTTPBearer:[]}])
HTTP: 204
[14:58:06] → delete http://localhost:8080/profiles/v1/likes/{target_user_id} (security: [{HTTPBearer:[]}])
HTTP: 204
[14:58:06] → get http://localhost:8080/match/v1/pairs/{pair_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → delete http://localhost:8080/match/v1/pairs/{pair_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → patch http://localhost:8080/match/v1/pairs/{pair_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → post http://localhost:8080/match/v1/pairs/{pair_id}/accept (security: [{OAuth2PasswordBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → post http://localhost:8080/match/v1/pairs/{pair_id}/reject (security: [{OAuth2PasswordBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → get http://localhost:8080/chat/v1/rooms/{room_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → get http://localhost:8080/chat/v1/rooms/{room_id}/messages (security: [{OAuth2PasswordBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → post http://localhost:8080/chat/v1/rooms/{room_id}/messages (security: [{OAuth2PasswordBearer:[]}])
HTTP: 422
{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}[14:58:06] → get http://localhost:8080/payments/v1/invoices/{inv_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 500
Internal Server Error[14:58:06] → delete http://localhost:8080/payments/v1/invoices/{inv_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → patch http://localhost:8080/payments/v1/invoices/{inv_id} (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] → post http://localhost:8080/payments/v1/invoices/{inv_id}/mark-paid (security: [{OAuth2PasswordBearer:[]}])
HTTP: 403
{"detail":"Insufficient role"}[14:58:06] Report: logs/audit_20250810_145805.json
[14:58:06] Log: logs/audit_20250810_145805.log

View File

@@ -0,0 +1,449 @@
{
"base_url": "http://localhost:8080",
"openapi_url": "http://localhost:8080/openapi.json",
"ts": "20250810_150332",
"summary": {
"missing_path_param_ops": 19
},
"findings": {
"missing_path_params": [
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
}
}

View File

@@ -0,0 +1,494 @@
[15:03:32] OpenAPI: http://localhost:8080/openapi.json
[15:03:32] ✔ OpenAPI fetched (32448 bytes)
[15:03:32] Prepare tokens
[15:03:32] REGISTER admin+20250810_150332@audit.dev (role=ADMIN) → http://localhost:8080/auth/v1/register
HTTP: 201
[15:03:32] TOKEN for admin+20250810_150332@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxN2M5ODNjZi02NzQ2LTRiZDctYTU2Yi01NGNmMWMxZGY4M2MiLCJlbWFpbCI6ImFkbWluKzIwMjUwODEwXzE1MDMzMkBhdWRpdC5kZXYiLCJyb2xlIjoiQURNSU4iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0ODA2NzEyfQ.hvizRRRhsJ3TwXB59kEDWsD5HopKdlnZcLN1NVowtXc","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxN2M5ODNjZi02NzQ2LTRiZDctYTU2Yi01NGNmMWMxZGY4M2MiLCJl[15:03:33] REGISTER client+20250810_150332@audit.dev (role=CLIENT) → http://localhost:8080/auth/v1/register
HTTP: 201
[15:03:33] TOKEN for client+20250810_150332@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiMDUxOTUyNi03MDMyLTRiNTEtODRkOS1mNWY2OWY2ODBkMTgiLCJlbWFpbCI6ImNsaWVudCsyMDI1MDgxMF8xNTAzMzJAYXVkaXQuZGV2Iiwicm9sZSI6IkNMSUVOVCIsInR5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NTQ4MDY3MTN9.qDEzkMrlcIfoJCztZdnBXNjc-LIse7mlqWvfz8IeyrQ","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiMDUxOTUyNi03MDMyLTRiNTEtODRkOS1mNWY2OWY2ODBkMTgiLC[15:03:33] ✔ Tokens acquired
[15:03:33] Analyze pathparameters in schema…
[15:03:33] ✖ Найдены операции без объявленных pathпараметров: 19
[
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
[15:03:33] SMOKE missing-param operations…
[15:03:33] → get http://localhost:8080/auth/v1/users/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"User not found"}[15:03:33] → delete http://localhost:8080/auth/v1/users/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 204
[15:03:33] → patch http://localhost:8080/auth/v1/users/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 422
{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}[15:03:33] → get http://localhost:8080/profiles/v1/profiles/00000000-0000-0000-0000-000000000000 (security: [{"HTTPBearer":[]}])
HTTP: 500
Internal Server Error[15:03:33] → get http://localhost:8080/profiles/v1/profiles/by-user/00000000-0000-0000-0000-000000000000 (security: [{"HTTPBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] → put http://localhost:8080/profiles/v1/likes/00000000-0000-0000-0000-000000000000 (security: [{"HTTPBearer":[]}])
HTTP: 204
[15:03:33] → delete http://localhost:8080/profiles/v1/likes/00000000-0000-0000-0000-000000000000 (security: [{"HTTPBearer":[]}])
HTTP: 204
[15:03:33] → get http://localhost:8080/match/v1/pairs/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] → delete http://localhost:8080/match/v1/pairs/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 204
[15:03:33] → patch http://localhost:8080/match/v1/pairs/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 422
{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}[15:03:33] → post http://localhost:8080/match/v1/pairs/00000000-0000-0000-0000-000000000000/accept (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] → post http://localhost:8080/match/v1/pairs/00000000-0000-0000-0000-000000000000/reject (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] → get http://localhost:8080/chat/v1/rooms/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] → get http://localhost:8080/chat/v1/rooms/00000000-0000-0000-0000-000000000000/messages (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Room not found"}[15:03:33] → post http://localhost:8080/chat/v1/rooms/00000000-0000-0000-0000-000000000000/messages (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Room not found"}[15:03:33] → get http://localhost:8080/payments/v1/invoices/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] → delete http://localhost:8080/payments/v1/invoices/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 204
[15:03:33] → patch http://localhost:8080/payments/v1/invoices/00000000-0000-0000-0000-000000000000 (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 422
{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null}]}[15:03:33] → post http://localhost:8080/payments/v1/invoices/00000000-0000-0000-0000-000000000000/mark-paid (security: [{"OAuth2PasswordBearer":[]}])
HTTP: 404
{"detail":"Not found"}[15:03:33] Report: logs/audit_20250810_150332.json
[15:03:33] Log: logs/audit_20250810_150332.log

View File

@@ -0,0 +1,449 @@
{
"base_url": "http://localhost:8080",
"openapi_url": "http://localhost:8080/openapi.json",
"ts": "20250810_150348",
"summary": {
"missing_path_param_ops": 19
},
"findings": {
"missing_path_params": [
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
}
}

View File

@@ -0,0 +1,445 @@
[15:03:48] OpenAPI: http://localhost:8080/openapi.json
[15:03:48] ✔ OpenAPI fetched (32448 bytes)
[15:03:48] Analyze pathparameters in schema…
[15:03:48] ✖ Найдены операции без объявленных pathпараметров: 19
[
{
"path": "/auth/v1/users/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_user_v1_users__user_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "delete",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "delete_user_v1_users__user_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/auth/v1/users/{user_id}",
"method": "patch",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "update_user_v1_users__user_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/{profile_id}",
"method": "get",
"needed": [
[
"profile_id"
]
],
"defined": [
"profile_id"
],
"missing": [
[
"profile_id"
]
],
"opId": "get_profile_v1_profiles__profile_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/profiles/by-user/{user_id}",
"method": "get",
"needed": [
[
"user_id"
]
],
"defined": [
"user_id"
],
"missing": [
[
"user_id"
]
],
"opId": "get_by_user_v1_profiles_by_user__user_id__get",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "put",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "put_like_v1_likes__target_user_id__put",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/profiles/v1/likes/{target_user_id}",
"method": "delete",
"needed": [
[
"target_user_id"
]
],
"defined": [
"target_user_id"
],
"missing": [
[
"target_user_id"
]
],
"opId": "delete_like_v1_likes__target_user_id__delete",
"security": [
{
"HTTPBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "get",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "get_pair_v1_pairs__pair_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "delete",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "delete_pair_v1_pairs__pair_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}",
"method": "patch",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "update_pair_v1_pairs__pair_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/accept",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "accept_v1_pairs__pair_id__accept_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/match/v1/pairs/{pair_id}/reject",
"method": "post",
"needed": [
[
"pair_id"
]
],
"defined": [
"pair_id"
],
"missing": [
[
"pair_id"
]
],
"opId": "reject_v1_pairs__pair_id__reject_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "get_room_v1_rooms__room_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "get",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "list_messages_v1_rooms__room_id__messages_get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/chat/v1/rooms/{room_id}/messages",
"method": "post",
"needed": [
[
"room_id"
]
],
"defined": [
"room_id"
],
"missing": [
[
"room_id"
]
],
"opId": "send_message_v1_rooms__room_id__messages_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "get",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "get_invoice_v1_invoices__inv_id__get",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "delete",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "delete_invoice_v1_invoices__inv_id__delete",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}",
"method": "patch",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "update_invoice_v1_invoices__inv_id__patch",
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
{
"path": "/payments/v1/invoices/{inv_id}/mark-paid",
"method": "post",
"needed": [
[
"inv_id"
]
],
"defined": [
"inv_id"
],
"missing": [
[
"inv_id"
]
],
"opId": "mark_paid_v1_invoices__inv_id__mark_paid_post",
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
]
[15:03:48] Report: logs/audit_20250810_150348.json
[15:03:48] Log: logs/audit_20250810_150348.log

View File

@@ -0,0 +1,11 @@
{
"base_url": "http://localhost:8080",
"openapi_url": "http://localhost:8080/openapi.json",
"ts": "20250810_153547",
"summary": {
"missing_path_param_ops": 0
},
"findings": {
"missing_path_params": []
}
}

View File

@@ -0,0 +1,16 @@
[15:35:47] OpenAPI: http://localhost:8080/openapi.json
[15:35:47] ✔ OpenAPI fetched (32448 bytes)
[15:35:47] Prepare tokens
[15:35:48] REGISTER admin+20250810_153547@audit.dev (role=ADMIN) → http://localhost:8080/auth/v1/register
HTTP: 201
[15:35:48] TOKEN for admin+20250810_153547@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0YjczYjYxMy0wYTFlLTRkODMtYjM3NC1hYWY5OWY4ODVmZDQiLCJlbWFpbCI6ImFkbWluKzIwMjUwODEwXzE1MzU0N0BhdWRpdC5kZXYiLCJyb2xlIjoiQURNSU4iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0ODA4NjQ4fQ.7AAL-8NVK3NiJaN0y4iUeZvYdvWnym05R-L9zeqwMr8","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0YjczYjYxMy0wYTFlLTRkODMtYjM3NC1hYWY5OWY4ODVmZDQiLCJl[15:35:48] REGISTER client+20250810_153547@audit.dev (role=CLIENT) → http://localhost:8080/auth/v1/register
HTTP: 201
[15:35:48] TOKEN for client+20250810_153547@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkMzFjZGFjOS1kYmZjLTQ0NTMtYWVkZi01Y2Q2YWJkZDFhYjciLCJlbWFpbCI6ImNsaWVudCsyMDI1MDgxMF8xNTM1NDdAYXVkaXQuZGV2Iiwicm9sZSI6IkNMSUVOVCIsInR5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NTQ4MDg2NDh9.xRQXWYLDKsgRfvQRc_GN4bEnLsTgctQKAiXImJJsnxE","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkMzFjZGFjOS1kYmZjLTQ0NTMtYWVkZi01Y2Q2YWJkZDFhYjciLC[15:35:48] ✔ Tokens acquired
[15:35:48] Analyze pathparameters in schema…
[15:35:48] ✔ Проблем с pathпараметрами в схеме не найдено.
[15:35:48] Report: logs/audit_20250810_153547.json
[15:35:48] Log: logs/audit_20250810_153547.log

View File

@@ -0,0 +1,11 @@
[14:38:46] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ No missing path-param definitions in schema.
[14:38:46] Prepare tokens
[14:38:46] REGISTER admin+1754804326@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"b661b2ce-8109-4481-b126-0ac4ab4712dc","email":"admin+1754804326@audit.dev","full_name":"Audit Admin","role":"ADMIN","is_active":true}[14:38:46] REGISTER client+1754804326@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"ad3ddf95-3bf4-4de0-9c01-d8bfa7e9afaa","email":"client+1754804326@audit.dev","full_name":"Audit Client","role":"CLIENT","is_active":true}[14:38:46] TOKEN for admin+1754804326@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiNjYxYjJjZS04MTA5LTQ0ODEtYjEyNi0wYWM0YWI0NzEyZGMiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQzMjZAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTIyN30.qekpbC4oywTg6GUsZrwLkLi6InadqlAyg9IlXSWiuy8","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiNjYxYjJjZS04MTA5LTQ0ODEtYjEyNi0wYWM0YWI0NzEyZGMiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQzMjZAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTYzMjd9.fgzpO2kGqQIDjZpddufaoZwNliSIKs78MOOs6vEuxQA","token_type":"bearer"}200|/tmp/tmp.MQwQIiPD4R/body_1754804327177054908.json✖ Empty token in response

View File

@@ -0,0 +1,11 @@
[14:39:30] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ No missing path-param definitions in schema.
[14:39:30] Prepare tokens
[14:39:30] REGISTER admin+1754804370@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"d895702e-003e-4c76-b881-3dd9013ad718","email":"admin+1754804370@audit.dev","full_name":"Audit Admin","role":"ADMIN","is_active":true}[14:39:31] REGISTER client+1754804370@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"9de89380-7dc1-4c9b-a725-6b3892dd4d86","email":"client+1754804370@audit.dev","full_name":"Audit Client","role":"CLIENT","is_active":true}[14:39:31] TOKEN for admin+1754804370@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkODk1NzAyZS0wMDNlLTRjNzYtYjg4MS0zZGQ5MDEzYWQ3MTgiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQzNzBAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTI3MX0.ctbEhzmwPRc_1nyxc4-SYohdTmM__H2Afe4csjktamQ","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkODk1NzAyZS0wMDNlLTRjNzYtYjg4MS0zZGQ5MDEzYWQ3MTgiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQzNzBAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTYzNzF9.h1tbqmU75s4IveybFQJTZB6cx5O-HdRr8QDS5ufcdgs","token_type":"bearer"}200|/tmp/tmp.domipoILvA/body_1754804371694718750.json✖ Empty token in response

View File

@@ -0,0 +1,11 @@
[14:39:52] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ No missing path-param definitions in schema.
[14:39:52] Prepare tokens
[14:39:52] REGISTER admin+1754804392@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"4ea8cfdf-411a-4156-a708-20e1f937c0c7","email":"admin+1754804392@audit.dev","full_name":"Audit Admin","role":"ADMIN","is_active":true}[14:39:52] REGISTER client+1754804392@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"606ec141-5a41-4ae4-a01d-0e271dca5e76","email":"client+1754804392@audit.dev","full_name":"Audit Client","role":"CLIENT","is_active":true}[14:39:52] TOKEN for admin+1754804392@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0ZWE4Y2ZkZi00MTFhLTQxNTYtYTcwOC0yMGUxZjkzN2MwYzciLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQzOTJAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTI5M30.bMvuxJyAHV9xQT49bQlcndEacujplwvQIwKc2LBcu8E","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0ZWE4Y2ZkZi00MTFhLTQxNTYtYTcwOC0yMGUxZjkzN2MwYzciLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQzOTJAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTYzOTN9.fOLyAp8-JLsF5ntSvcvbmbarYPlH33xJJzBIqOtoHj8","token_type":"bearer"}200|/tmp/tmp.8a4EKh0yJd/body_1754804393153931372.json✖ Empty token in response

View File

@@ -0,0 +1,11 @@
[14:41:26] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ Проблем с pathпараметрами в схеме не найдено.
[14:41:26] Prepare tokens
[14:41:26] REGISTER admin+1754804486@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"fa614a25-1a90-4219-8d99-328ccb6261af","email":"admin+1754804486@audit.dev","full_name":"Audit Admin","role":"ADMIN","is_active":true}[14:41:26] REGISTER client+1754804486@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"b7aca4cd-7321-4666-83f4-6dcd2f41d4e4","email":"client+1754804486@audit.dev","full_name":"Audit Client","role":"CLIENT","is_active":true}[14:41:26] TOKEN for admin+1754804486@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmYTYxNGEyNS0xYTkwLTQyMTktOGQ5OS0zMjhjY2I2MjYxYWYiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ0ODZAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTM4Nn0.GB4lBfmEzpkXJ6N51URV4RR1w4096jdL3YSwTxzlF2M","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmYTYxNGEyNS0xYTkwLTQyMTktOGQ5OS0zMjhjY2I2MjYxYWYiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ0ODZAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTY0ODZ9.-JhPKTCY0t3SpQmoLA6iKYadisMyESrn-rWu1yvV6lg","token_type":"bearer"}200|/tmp/tmp.IxGgVqPsBj/h_1754804486719735758.txt|/tmp/tmp.IxGgVqPsBj/b_1754804486720782176.txt✖ Empty access_token in response. See body above.

View File

@@ -0,0 +1,10 @@
[14:42:51] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ Проблем с pathпараметрами в схеме не найдено.
[14:42:51] REGISTER admin+1754804571@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"5159a9c9-2cb2-439e-8fba-f074df6f3f62","email":"admin+1754804571@audit.dev","full_name":"Audit ADMIN","role":"ADMIN","is_active":true}[14:42:51] REGISTER client+1754804571@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"0a1261da-cd58-45d2-b2ac-ba1cbca3ed04","email":"client+1754804571@audit.dev","full_name":"Audit CLIENT","role":"CLIENT","is_active":true}[14:42:51] TOKEN for admin+1754804571@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1MTU5YTljOS0yY2IyLTQzOWUtOGZiYS1mMDc0ZGY2ZjNmNjIiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ1NzFAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTQ3Mn0.tNxZJFWwd_z-6gPc8WD05_EaB5-ZR-OhHjzqC84-rzI","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1MTU5YTljOS0yY2IyLTQzOWUtOGZiYS1mMDc0ZGY2ZjNmNjIiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ1NzFAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTY1NzJ9.kPQdnZArXVW0YlhJLcjfSLDz0bZkNanVuv_ORofjZHw","token_type":"bearer"}✖ Empty access_token in response

View File

@@ -0,0 +1,10 @@
[14:44:45] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ Проблем с pathпараметрами в схеме не найдено.
[14:44:45] REGISTER admin+1754804685@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"240ec6b3-f808-4c7f-b34e-94ef2d116d02","email":"admin+1754804685@audit.dev","full_name":"Audit ADMIN","role":"ADMIN","is_active":true}[14:44:45] REGISTER client+1754804685@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"7bbcf608-e287-47bb-b764-f9a8a6dd51ef","email":"client+1754804685@audit.dev","full_name":"Audit CLIENT","role":"CLIENT","is_active":true}[14:44:45] TOKEN for admin+1754804685@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyNDBlYzZiMy1mODA4LTRjN2YtYjM0ZS05NGVmMmQxMTZkMDIiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ2ODVAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTU4NX0.Dqpib2vUHp01UG9fnzpTheJFlHx7YSIzb-Nos8mDZiA","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyNDBlYzZiMy1mODA4LTRjN2YtYjM0ZS05NGVmMmQxMTZkMDIiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ2ODVAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTY2ODV9.6oaupNjTprbAuJ3IodqJJePgoOUpiB-hsVYYKc94mdE","token_type":"bearer"}✖ Empty access_token in response

View File

@@ -0,0 +1,14 @@
[14:47:49] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ Проблем с pathпараметрами в схеме не найдено.
[14:47:49] REGISTER admin+1754804869@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"e59260d0-9b68-4b34-bb99-79f2388db828","email":"admin+1754804869@audit.dev","full_name":"Audit ADMIN","role":"ADMIN","is_active":true}[14:47:49] REGISTER client+1754804869@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"24fc407d-3b73-4f43-aec9-75a5acb43957","email":"client+1754804869@audit.dev","full_name":"Audit CLIENT","role":"CLIENT","is_active":true}[14:47:50] TOKEN for admin+1754804869@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNTkyNjBkMC05YjY4LTRiMzQtYmI5OS03OWYyMzg4ZGI4MjgiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ4NjlAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTc3MH0.RLw5GBq3ti79qdt3FTvMCcL4J_wDhR-UDZatVYTJrbo","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNTkyNjBkMC05YjY4LTRiMzQtYmI5OS03OWYyMzg4ZGI4MjgiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ4NjlAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTY4NzB9.YkAPwLUHuIjz40-R-OGe7RqxD3PpfnP9qf90AFap-Yk","token_type":"bearer"}
[14:47:50] TOKEN for client+1754804869@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyNGZjNDA3ZC0zYjczLTRmNDMtYWVjOS03NWE1YWNiNDM5NTciLCJlbWFpbCI6ImNsaWVudCsxNzU0ODA0ODY5QGF1ZGl0LmRldiIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0ODA1NzcwfQ.hbPMN71FWv7E1lORHVwIO-i0E92wiV6wvrvv-s2JeQU","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyNGZjNDA3ZC0zYjczLTRmNDMtYWVjOS03NWE1YWNiNDM5NTciLCJlbWFpbCI6ImNsaWVudCsxNzU0ODA0ODY5QGF1ZGl0LmRldiIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc1NzM5Njg3MH0.5rHQJ-11xduNN5RZ-qVc0eFn6usQsCdDaPNsUZU2zwQ","token_type":"bearer"}
✔ Tokens acquired

View File

@@ -0,0 +1,14 @@
[14:49:35] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ Проблем с pathпараметрами в схеме не найдено.
[14:49:35] REGISTER admin+1754804975@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"379e9211-32ec-4135-9e82-1f550e4e1269","email":"admin+1754804975@audit.dev","full_name":"Audit ADMIN","role":"ADMIN","is_active":true}[14:49:35] REGISTER client+1754804975@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"6ca28dcd-f7c2-4a60-bed3-57b412b00404","email":"client+1754804975@audit.dev","full_name":"Audit CLIENT","role":"CLIENT","is_active":true}[14:49:36] TOKEN for admin+1754804975@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzllOTIxMS0zMmVjLTQxMzUtOWU4Mi0xZjU1MGU0ZTEyNjkiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ5NzVAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwNTg3Nn0.qjZ1pkegHeGwN5mXez1G6PzDoSmRnTiYzVjYbxcDJ2g","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzllOTIxMS0zMmVjLTQxMzUtOWU4Mi0xZjU1MGU0ZTEyNjkiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDQ5NzVAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTY5NzZ9.Hyg05Y_W-oSpvvC0vyJ_hECSQEE0nQv6VniZQvX4yH8","token_type":"bearer"}
[14:49:36] TOKEN for client+1754804975@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2Y2EyOGRjZC1mN2MyLTRhNjAtYmVkMy01N2I0MTJiMDA0MDQiLCJlbWFpbCI6ImNsaWVudCsxNzU0ODA0OTc1QGF1ZGl0LmRldiIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0ODA1ODc2fQ.iKXVOwmi3kTQsK2DyDtgR4vCL4ASDF7t3Oo9oDSCtuA","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2Y2EyOGRjZC1mN2MyLTRhNjAtYmVkMy01N2I0MTJiMDA0MDQiLCJlbWFpbCI6ImNsaWVudCsxNzU0ODA0OTc1QGF1ZGl0LmRldiIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc1NzM5Njk3Nn0.z7N5eYxIO3pR9Y99x44_tHDQ1zBLa8vQop1Rsz1Kg-A","token_type":"bearer"}
✔ Tokens acquired

View File

@@ -0,0 +1,14 @@
[15:35:53] OPENAPI: http://localhost:8080/openapi.json
✔ OpenAPI fetched (32448 bytes)
✔ Проблем с pathпараметрами в схеме не найдено.
[15:35:53] REGISTER admin+1754807753@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"8913ac48-c289-43b2-87db-a37aa5e39548","email":"admin+1754807753@audit.dev","full_name":"Audit ADMIN","role":"ADMIN","is_active":true}[15:35:54] REGISTER client+1754807753@audit.dev → http://localhost:8080/auth/v1/register
HTTP: 201
{"id":"0f4a02da-d0fa-494e-abbf-a2968ad44769","email":"client+1754807753@audit.dev","full_name":"Audit CLIENT","role":"CLIENT","is_active":true}[15:35:54] TOKEN for admin+1754807753@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4OTEzYWM0OC1jMjg5LTQzYjItODdkYi1hMzdhYTVlMzk1NDgiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDc3NTNAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDgwODY1NH0.YRh6bpLSSdGVDPqnSX4pEKPOshqqSjBNi54LUEICI70","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4OTEzYWM0OC1jMjg5LTQzYjItODdkYi1hMzdhYTVlMzk1NDgiLCJlbWFpbCI6ImFkbWluKzE3NTQ4MDc3NTNAYXVkaXQuZGV2Iiwicm9sZSI6IkFETUlOIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTczOTk3NTR9.HGPD33LyxA32Q6o_Vf93D8nUfqxPKuT9hxZXawptXO8","token_type":"bearer"}
[15:35:54] TOKEN for client+1754807753@audit.dev → http://localhost:8080/auth/v1/token
HTTP: 200
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwZjRhMDJkYS1kMGZhLTQ5NGUtYWJiZi1hMjk2OGFkNDQ3NjkiLCJlbWFpbCI6ImNsaWVudCsxNzU0ODA3NzUzQGF1ZGl0LmRldiIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0ODA4NjU0fQ.o0q7ADkiX8uyfd_5U_A5ikcjDWKGOZyxA7Qn5Mkg3zY","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwZjRhMDJkYS1kMGZhLTQ5NGUtYWJiZi1hMjk2OGFkNDQ3NjkiLCJlbWFpbCI6ImNsaWVudCsxNzU0ODA3NzUzQGF1ZGl0LmRldiIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc1NzM5OTc1NH0.rdHhue9u-Xe-xq2EdQvV2fc0FuYV6CiVv7BXIW56fY4","token_type":"bearer"}
✔ Tokens acquired

View File

@@ -0,0 +1,79 @@
-----
[2025-08-10 14:20:09] BASE_URL: http://localhost:8080
[2025-08-10 14:20:09] EMAIL: user+1754803209@example.com
[2025-08-10 14:20:09] FULL_NAME: Demo User
[2025-08-10 14:20:09] ROLE: CLIENT
-----
[2025-08-10 14:20:09] REGISTER
[2025-08-10 14:20:09] URL: http://localhost:8080/auth/v1/register
[2025-08-10 14:20:09] HTTP: 201
[2025-08-10 14:20:09] Body:
{
"id": "f4ffd570-5f43-4268-ae6c-cf3f8ec57793",
"email": "user+1754803209@example.com",
"full_name": "Demo User",
"role": "CLIENT",
"is_active": true
}
-----
[2025-08-10 14:20:09] LOGIN / TOKEN
[2025-08-10 14:20:09] URL: http://localhost:8080/auth/v1/token
[2025-08-10 14:20:09] HTTP: 200
[2025-08-10 14:20:09] Body:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmNGZmZDU3MC01ZjQzLTQyNjgtYWU2Yy1jZjNmOGVjNTc3OTMiLCJlbWFpbCI6InVzZXIrMTc1NDgwMzIwOUBleGFtcGxlLmNvbSIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0ODA0MTA5fQ.HgFmBf7lVPTFAiS91k-Ezh5DDjbdFCcMX9hdxY6-YcM",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmNGZmZDU3MC01ZjQzLTQyNjgtYWU2Yy1jZjNmOGVjNTc3OTMiLCJlbWFpbCI6InVzZXIrMTc1NDgwMzIwOUBleGFtcGxlLmNvbSIsInJvbGUiOiJDTElFTlQiLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc1NzM5NTIwOX0.bepjGzYMGld7o_KB39Z3K7nYmt4plVKotvF37E8zawA",
"token_type": "bearer"
}
[2025-08-10 14:20:09] Got access_token (hidden)
-----
-----
[2025-08-10 14:20:09] GET /profiles/me
[2025-08-10 14:20:09] URL: http://localhost:8080/profiles/v1/profiles/me
[2025-08-10 14:20:09] HTTP: 404
[2025-08-10 14:20:09] Body:
{
"detail": "Profile not found"
}
[2025-08-10 14:20:09] Profile not found — creating…
-----
[2025-08-10 14:20:09] CREATE /profiles
[2025-08-10 14:20:09] URL: http://localhost:8080/profiles/v1/profiles
[2025-08-10 14:20:09] HTTP: 201
[2025-08-10 14:20:09] Body:
{
"gender": "other",
"city": "Moscow",
"languages": [
"ru",
"en"
],
"interests": [
"music",
"travel"
],
"id": "aa6d6046-b235-4727-a37d-9b74f36cce00",
"user_id": "f4ffd570-5f43-4268-ae6c-cf3f8ec57793",
"photo_url": null
}
-----
[2025-08-10 14:20:09] GET /profiles/me (after create)
[2025-08-10 14:20:09] URL: http://localhost:8080/profiles/v1/profiles/me
[2025-08-10 14:20:09] HTTP: 200
[2025-08-10 14:20:09] Body:
{
"gender": "other",
"city": "Moscow",
"languages": [
"ru",
"en"
],
"interests": [
"music",
"travel"
],
"id": "aa6d6046-b235-4727-a37d-9b74f36cce00",
"user_id": "f4ffd570-5f43-4268-ae6c-cf3f8ec57793",
"photo_url": null
}
[2025-08-10 14:20:09] DONE. Log saved to: logs/user_flow_20250810_142009.log

View File

@@ -0,0 +1 @@
'"$@"'

View File

@@ -0,0 +1 @@
'"$@"'

View File

@@ -1,134 +1,310 @@
#!/usr/bin/env bash
set -euo pipefail
CONF="infra/gateway/nginx.conf"
[[ -f "$CONF" ]] || { echo "[ERR] $CONF not found"; exit 1; }
cp "$CONF" "$CONF.bak.$(date +%s)"
echo "[OK] backup saved"
cat > "$CONF" <<'NGINX'
server {
listen 80;
server_name _;
# Docker DNS
resolver 127.0.0.11 ipv6=off valid=10s;
# Health of gateway itself
location = /health {
default_type application/json;
return 200 '{"status":"ok","gateway":"nginx"}';
}
# ===== Unified API Docs (docs aggregator) =====
location = /docs {
proxy_pass http://marriage_docs:8000/docs;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /redoc {
proxy_pass http://marriage_docs:8000/redoc;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /openapi.json {
proxy_pass http://marriage_docs:8000/openapi.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /refresh {
proxy_pass http://marriage_docs:8000/refresh;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /_health {
proxy_pass http://marriage_docs:8000/_health;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ===== Microservices (prefix strip) =====
location /auth/ {
rewrite ^/auth/(.*)$ /$1 break;
proxy_pass http://marriage_auth:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
}
location /profiles/ {
rewrite ^/profiles/(.*)$ /$1 break;
proxy_pass http://marriage_profiles:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /match/ {
rewrite ^/match/(.*)$ /$1 break;
proxy_pass http://marriage_match:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /chat/ {
rewrite ^/chat/(.*)$ /$1 break;
proxy_pass http://marriage_chat:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /payments/ {
rewrite ^/payments/(.*)$ /$1 break;
proxy_pass http://marriage_payments:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
write() {
local path="$1"; shift
mkdir -p "$(dirname "$path")"
if [[ -f "$path" ]]; then cp -f "$path" "${path}.bak"; fi
cat >"$path" <<'PYEOF'
'"$@"'
PYEOF
echo "✔ wrote $path"
}
NGINX
echo "[OK] nginx.conf updated"
# ---------- chat/src/app/api/chat.py ----------
write chat/src/app/api/chat.py '
from __future__ import annotations
from typing import List
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
echo "[INFO] restarting gateway..."
docker compose restart gateway >/dev/null
from app.db.session import get_db
from app.core.security import get_current_user, UserClaims
from app.schemas.chat import RoomCreate, RoomRead, MessageCreate, MessageRead
from app.services.chat_service import ChatService
echo "[INFO] quick checks:"
echo -n "gateway: "; curl -s http://localhost:8080/health; echo
echo -n "docs/_health:"; curl -s http://localhost:8080/_health; echo
for svc in auth profiles match chat payments; do
code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/$svc/health")
echo "$svc/health: $code"
done
router = APIRouter(prefix="/v1", tags=["chat"])
@router.post("/rooms", response_model=RoomRead, status_code=201)
def create_room(payload: RoomCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ChatService(db)
room = svc.create_room(title=payload.title, participant_ids=payload.participants, creator_id=user.sub)
return room
@router.get("/rooms", response_model=list[RoomRead])
def my_rooms(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
return ChatService(db).list_rooms_for_user(user.sub)
@router.get("/rooms/{room_id}", response_model=RoomRead)
def get_room(room_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
room = ChatService(db).get_room(room_id, user.sub)
if not room:
raise HTTPException(status_code=404, detail="Room not found")
return room
@router.post("/rooms/{room_id}/messages", response_model=MessageRead, status_code=201)
def send_message(room_id: UUID, payload: MessageCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ChatService(db)
room = svc.get_room(room_id, user.sub)
if not room:
raise HTTPException(status_code=404, detail="Room not found")
msg = svc.create_message(room_id, user.sub, payload.content)
return msg
@router.get("/rooms/{room_id}/messages", response_model=list[MessageRead])
def list_messages(
room_id: UUID,
offset: int = 0,
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user),
):
svc = ChatService(db)
room = svc.get_room(room_id, user.sub)
if not room:
raise HTTPException(status_code=404, detail="Room not found")
return svc.list_messages(room_id, user.sub, offset, limit)
'
# ---------- match/src/app/api/routes/pairs.py ----------
write match/src/app/api/routes/pairs.py '
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.security import get_current_user, require_roles, UserClaims
from app.schemas.pair import PairCreate, PairUpdate, PairRead
from app.services.pair_service import PairService
router = APIRouter(prefix="/v1/pairs", tags=["pairs"])
@router.post("", response_model=PairRead, status_code=201)
def create_pair(
payload: PairCreate,
db: Session = Depends(get_db),
user: UserClaims = Depends(require_roles("ADMIN","MATCHMAKER")),
):
svc = PairService(db)
return svc.create(
user_id_a=payload.user_id_a,
user_id_b=payload.user_id_b,
score=payload.score,
notes=payload.notes,
created_by=user.sub,
)
@router.get("", response_model=list[PairRead])
def list_pairs(
for_user_id: str | None = None,
status: str | None = None,
offset: int = 0,
limit: int = Query(50, le=200),
db: Session = Depends(get_db),
_: UserClaims = Depends(get_current_user),
):
return PairService(db).list(for_user_id=for_user_id, status=status, offset=offset, limit=limit)
@router.get("/{pair_id}", response_model=PairRead)
def get_pair(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
pair = PairService(db).get(pair_id)
if not pair:
raise HTTPException(status_code=404, detail="Pair not found")
return pair
@router.patch("/{pair_id}", response_model=PairRead)
def update_pair(
pair_id: UUID,
payload: PairUpdate,
db: Session = Depends(get_db),
user: UserClaims = Depends(require_roles("ADMIN")),
):
updated = PairService(db).update(pair_id, payload)
if not updated:
raise HTTPException(status_code=404, detail="Pair not found")
return updated
@router.post("/{pair_id}/accept", response_model=PairRead)
def accept(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
res = PairService(db).accept(pair_id, user.sub)
if not res:
raise HTTPException(status_code=404, detail="Pair not found")
return res
@router.post("/{pair_id}/reject", response_model=PairRead)
def reject(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
res = PairService(db).reject(pair_id, user.sub)
if not res:
raise HTTPException(status_code=404, detail="Pair not found")
return res
@router.delete("/{pair_id}", status_code=204)
def delete_pair(
pair_id: UUID,
db: Session = Depends(get_db),
_: UserClaims = Depends(require_roles("ADMIN","MATCHMAKER")),
):
svc = PairService(db)
obj = svc.get(pair_id)
if not obj:
return
svc.delete(obj)
'
# ---------- profiles/src/app/api/routes/profiles.py ----------
write profiles/src/app/api/routes/profiles.py '
from __future__ import annotations
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Query, status
from sqlalchemy.orm import Session
import os
from ...core.security import get_current_user, require_roles, UserClaims
from ...db.session import get_db
from ...models.profile import Profile
from ...schemas.profile import ProfileCreate, ProfileUpdate, ProfileOut, LikesList
from ...services.profile_service import ProfileService
from ...services.profile_search_service import ProfileSearchService
from ...services.likes_service import LikesService
router = APIRouter(prefix="/v1/profiles", tags=["profiles"])
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")
BASE_EXTERNAL_URL = os.getenv("BASE_EXTERNAL_URL", "http://localhost:8080")
@router.get("/me", response_model=ProfileOut)
def get_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.post("", response_model=ProfileOut, status_code=201)
def create_profile(payload: ProfileCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
if svc.get_by_user_id(user.sub):
raise HTTPException(status_code=409, detail="Profile already exists")
return svc.create(user.sub, **payload.model_dump())
@router.get("", response_model=List[ProfileOut])
def list_profiles(
q: Optional[str] = None,
gender: Optional[str] = Query(None, pattern="^(male|female|other)$"),
city: Optional[str] = None,
languages: Optional[List[str]] = Query(None),
interests: Optional[List[str]] = Query(None),
has_photo: Optional[bool] = None,
sort_by: Optional[str] = Query(None, pattern="^(created_at|updated_at|city|gender)$"),
order: Optional[str] = Query("asc", pattern="^(asc|desc)$"),
offset: int = 0,
limit: int = Query(50, le=200),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user),
):
svc = ProfileSearchService(db)
return svc.list_profiles(
q=q, gender=gender, city=city,
languages=languages, interests=interests, has_photo=has_photo,
sort_by=sort_by, order=order, offset=offset, limit=limit,
)
@router.get("/{profile_id}", response_model=ProfileOut)
def get_profile(profile_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get(profile_id)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.get("/by-user/{user_id}", response_model=ProfileOut)
def get_by_user(user_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user_id)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.patch("/me", response_model=ProfileOut)
def patch_me(payload: ProfileUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
data = payload.model_dump(exclude_none=True)
return svc.update(prof, **data)
@router.delete("/me", status_code=204)
def delete_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
return
svc.delete(prof)
return
@router.post("/me/photo", response_model=ProfileOut)
def upload_photo(
file: UploadFile = File(...),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user),
):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if file.content_type not in ("image/jpeg", "image/png", "image/webp"):
raise HTTPException(status_code=400, detail="Invalid content-type")
os.makedirs(UPLOAD_DIR, exist_ok=True)
ext = {"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp"}[file.content_type]
subdir = os.path.join(UPLOAD_DIR, "avatars")
os.makedirs(subdir, exist_ok=True)
filename = f"{user.sub}.{ext}"
path = os.path.join(subdir, filename)
with open(path, "wb") as f:
f.write(file.file.read())
public_url = f"{BASE_EXTERNAL_URL}/profiles/static/avatars/{filename}"
return svc.update(prof, photo_url=public_url)
@router.delete("/me/photo", response_model=ProfileOut)
def delete_photo(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
return svc.update(prof, photo_url=None)
# Back-compat stub (hidden from schema)
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
def _compat_likes_redirect():
return LikesList(items=[])
likes_router = APIRouter(prefix="/v1/likes", tags=["profiles"])
@likes_router.get("", response_model=LikesList)
def my_likes(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
items = LikesService(db).list_my_likes(user.sub)
return LikesList(items=items)
@likes_router.put("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def put_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).put_like(user.sub, target_user_id)
return
@likes_router.delete("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).delete_like(user.sub, target_user_id)
return
@likes_router.get("/mutual", response_model=List[str])
def mutual(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
return LikesService(db).mutual(user.sub)
'
echo
echo "Done. Backups saved as *.bak where files existed."
echo "Rebuild & restart affected services, then re-run your audit:"
echo " docker compose build chat match profiles && docker compose up -d"
echo " ./scripts/audit.sh"

View File

@@ -1,29 +1,175 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# 1) Здоровье сервисов
curl -sS http://localhost:8080/auth/health
curl -sS http://localhost:8080/profiles/health
# === Settings ===
BASE_URL="${BASE_URL:-http://localhost:8080}"
AUTH="$BASE_URL/auth"
PROFILES="$BASE_URL/profiles"
# 2) Токен (любой юзер)
curl -sS -X POST http://localhost:8080/auth/v1/token \
-H "Content-Type: application/json" \
-d '{"email":"admin@agency.dev","password":"secret123"}' | tee /tmp/token.json
# Можно переопределить через окружение:
EMAIL="${EMAIL:-user+$(date +%s)@example.com}"
PASS="${PASS:-secret123}"
FULL_NAME="${FULL_NAME:-Demo User}"
ROLE="${ROLE:-CLIENT}"
ACCESS=$(python3 - <<'PY' /tmp/token.json
import sys, json; print(json.load(open(sys.argv[1]))["access_token"])
# Куда писать лог (по умолчанию в logs/)
mkdir -p logs
TS="$(date +%Y%m%d_%H%M%S)"
LOG_FILE="${LOG_FILE:-logs/user_flow_${TS}.log}"
# Вспомогательные утилиты
have_jq=0; command -v jq >/dev/null 2>&1 && have_jq=1
log() { echo "[$(date +'%F %T')] $*" | tee -a "$LOG_FILE" >&2; }
hr() { printf -- '-----\n' | tee -a "$LOG_FILE" >/dev/null; }
# Выполнить HTTP и вернуть: <http_code>|<body_file>
http_req() {
local METHOD="$1"; shift
local URL="$1"; shift
local TOKEN="${1:-}"; shift || true
local BODY="${1:-}"; shift || true
local RESP_FILE; RESP_FILE="$(mktemp)"
local args=(-sS --connect-timeout 10 --max-time 30 -X "$METHOD" "$URL" -o "$RESP_FILE" -w "%{http_code}")
[[ -n "$TOKEN" ]] && args+=(-H "Authorization: Bearer $TOKEN")
[[ -n "$BODY" ]] && args+=(-H "Content-Type: application/json" -d "$BODY")
local CODE; CODE="$(curl "${args[@]}" || true)"
echo "${CODE}|${RESP_FILE}"
}
# Красиво положить ответ в лог
log_response() {
local title="$1"; shift
local code="$1"; shift
local body_file="$1"; shift
hr
log "$title"
log "URL: ${CURRENT_URL}"
log "HTTP: ${code}"
if [[ $have_jq -eq 1 ]]; then
log "Body:"
jq . "$body_file" 2>/dev/null | tee -a "$LOG_FILE" >/dev/null || cat "$body_file" | tee -a "$LOG_FILE" >/dev/null
else
log "Body (raw):"
cat "$body_file" | tee -a "$LOG_FILE" >/dev/null
fi
}
# Достаём поле из JSON (без jq)
json_get() {
local file="$1"; shift
local path="$1"; shift
python3 - "$file" "$path" <<'PY'
import sys, json
fn, path = sys.argv[1], sys.argv[2]
try:
data = json.load(open(fn, 'rb'))
except Exception:
print('')
sys.exit(0)
cur = data
for k in path.split('.'):
if isinstance(cur, list):
try:
k = int(k)
except Exception:
print(''); sys.exit(0)
if 0 <= k < len(cur):
cur = cur[k]
else:
print(''); sys.exit(0)
elif isinstance(cur, dict):
cur = cur.get(k)
else:
print(''); sys.exit(0)
if cur is None:
print(''); sys.exit(0)
if isinstance(cur, (dict, list)):
print(json.dumps(cur))
else:
print(cur)
PY
)
}
# 3) /me — ожидаемо 404 (если профиля нет), главное НЕ 401
curl -i -sS http://localhost:8080/profiles/v1/profiles/me \
-H "Authorization: Bearer $ACCESS"
echo "== Log file: ${LOG_FILE} ==" >&2
hr
log "BASE_URL: ${BASE_URL}"
log "EMAIL: ${EMAIL}"
log "FULL_NAME: ${FULL_NAME}"
log "ROLE: ${ROLE}"
# 4) Создать профиль — должно быть 201/200, без 500
curl -i -sS -X POST http://localhost:8080/profiles/v1/profiles \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-d '{"gender":"female","city":"Moscow","languages":["ru","en"],"interests":["music","travel"]}'
# 1) REGISTER
BODY_REG=$(printf '{"email":"%s","password":"%s","full_name":"%s","role":"%s"}' "$EMAIL" "$PASS" "$FULL_NAME" "$ROLE")
CURRENT_URL="$AUTH/v1/register"
resp="$(http_req POST "$CURRENT_URL" "" "$BODY_REG")"
code="${resp%%|*}"; body="${resp##*|}"
log_response "REGISTER" "$code" "$body"
# 5) Снова /me — теперь 200 с JSON (UUIDы как строки)
curl -sS http://localhost:8080/profiles/v1/profiles/me \
-H "Authorization: Bearer $ACCESS" | jq .
if [[ "$code" != "201" && "$code" != "200" ]]; then
# если уже существует — ок, продолжаем
detail="$(json_get "$body" "detail" || true)"
if [[ "$code" == "400" && "$detail" == "Email already in use" ]]; then
log "Register: email already exists — continue to login"
else
log "Register non-success: ${code} — continue to login anyway"
fi
fi
# 2) LOGIN (TOKEN)
BODY_LOGIN=$(printf '{"email":"%s","password":"%s"}' "$EMAIL" "$PASS")
CURRENT_URL="$AUTH/v1/token"
resp="$(http_req POST "$CURRENT_URL" "" "$BODY_LOGIN")"
code="${resp%%|*}"; body="${resp##*|}"
log_response "LOGIN / TOKEN" "$code" "$body"
if [[ "$code" != "200" ]]; then
log "ERROR: login failed with $code"
exit 1
fi
ACCESS_TOKEN="$(json_get "$body" "access_token")"
if [[ -z "$ACCESS_TOKEN" ]]; then
log "ERROR: access_token not found in login response"
exit 1
fi
log "Got access_token (hidden)"
hr
# 3) GET PROFILE (me)
CURRENT_URL="$PROFILES/v1/profiles/me"
resp="$(http_req GET "$CURRENT_URL" "$ACCESS_TOKEN")"
code="${resp%%|*}"; body="${resp##*|}"
log_response "GET /profiles/me" "$code" "$body"
if [[ "$code" == "404" ]]; then
log "Profile not found — creating…"
# 3a) CREATE PROFILE (минимальный)
BODY_CREATE='{"gender":"other","city":"Moscow","languages":["ru","en"],"interests":["music","travel"]}'
CURRENT_URL="$PROFILES/v1/profiles"
resp="$(http_req POST "$CURRENT_URL" "$ACCESS_TOKEN" "$BODY_CREATE")"
code="${resp%%|*}"; body="${resp##*|}"
log_response "CREATE /profiles" "$code" "$body"
if [[ "$code" != "201" && "$code" != "200" ]]; then
log "ERROR: create profile failed with $code"
exit 1
fi
# 3b) GET PROFILE AGAIN
CURRENT_URL="$PROFILES/v1/profiles/me"
resp="$(http_req GET "$CURRENT_URL" "$ACCESS_TOKEN")"
code="${resp%%|*}"; body="${resp##*|}"
log_response "GET /profiles/me (after create)" "$code" "$body"
if [[ "$code" != "200" ]]; then
log "ERROR: expected 200 after create, got $code"
exit 1
fi
elif [[ "$code" != "200" ]]; then
log "ERROR: unexpected code for /profiles/me: $code"
exit 1
fi
log "DONE. Log saved to: ${LOG_FILE}"

View File

@@ -7,6 +7,8 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
RUN mkdir -p /app/uploads
COPY src ./src
COPY alembic.ini ./

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e
# Run migrations (no-op if no revisions yet)
alembic -c alembic.ini upgrade head || true
# Start app
exec uvicorn app.main:app --host 0.0.0.0 --port 8000

View File

@@ -0,0 +1,26 @@
from typing import Optional, List
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from ...db.session import get_db
from ...core.security import require_roles, UserClaims
from ...schemas.user import UserRead
from ...repositories.user_search_repository import UserSearchRepository
router = APIRouter(prefix="/v1/users", tags=["users"])
@router.get("/search", response_model=List[UserRead])
def search_users(q: Optional[str] = None,
role: Optional[str] = Query(None, pattern="^(ADMIN|CLIENT)$"),
is_active: Optional[bool] = None,
email_domain: Optional[str] = None,
created_from: Optional[str] = None,
created_to: Optional[str] = None,
sort_by: Optional[str] = Query(None, pattern="^(full_name|email|created_at|role|is_active)$"),
order: Optional[str] = Query("asc", pattern="^(asc|desc)$"),
offset: int = 0, limit: int = Query(50, le=200),
db: Session = Depends(get_db),
_: UserClaims = Depends(require_roles("ADMIN"))):
repo = UserSearchRepository(db)
return repo.search(q=q, role=role, is_active=is_active, email_domain=email_domain,
created_from=created_from, created_to=created_to,
sort_by=sort_by, order=order, offset=offset, limit=limit)

View File

@@ -0,0 +1,14 @@
from fastapi import FastAPI
from .api.routes.ping import router as ping_router
from .api.routes.auth import router as auth_router
from .api.routes.users import router as users_router
app = FastAPI(title="AUTH Service")
@app.get("/health")
def health():
return {"status": "ok", "service": "auth"}
app.include_router(ping_router, prefix="/v1")
app.include_router(auth_router)
app.include_router(users_router)

View File

@@ -0,0 +1,38 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import or_, func
from ..models.user import User
class UserSearchRepository:
def __init__(self, db: Session):
self.db = db
def search(self, q: Optional[str], role: Optional[str], is_active: Optional[bool],
email_domain: Optional[str], created_from: Optional[str], created_to: Optional[str],
sort_by: Optional[str], order: Optional[str], offset: int, limit: int) -> List[User]:
qry = self.db.query(User)
if q:
like = f"%{q}%"
qry = qry.filter(or_(User.email.ilike(like), User.full_name.ilike(like)))
if role:
qry = qry.filter(User.role == role)
if is_active is not None:
qry = qry.filter(User.is_active == is_active)
if email_domain:
qry = qry.filter(User.email.ilike(f"%@{email_domain}"))
if created_from:
qry = qry.filter(User.created_at >= created_from)
if created_to:
qry = qry.filter(User.created_at <= created_to)
sort_map = {
"full_name": User.full_name,
"email": User.email,
"created_at": User.created_at,
"role": User.role,
"is_active": User.is_active
}
col = sort_map.get(sort_by or "", User.created_at)
if order == "desc":
col = col.desc()
return qry.order_by(col).offset(offset).limit(min(limit, 200)).all()

View File

@@ -7,6 +7,8 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
RUN mkdir -p /app/uploads
COPY src ./src
COPY alembic.ini ./

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e
# Run migrations (no-op if no revisions yet)
alembic -c alembic.ini upgrade head || true
# Start app
exec uvicorn app.main:app --host 0.0.0.0 --port 8000

View File

@@ -6,6 +6,7 @@ from app.db.session import get_db
from app.core.security import get_current_user, UserClaims
from app.schemas.chat import RoomCreate, RoomRead, MessageCreate, MessageRead
from app.services.chat_service import ChatService
from fastapi import APIRouter, Depends, HTTPException
router = APIRouter(prefix="/v1", tags=["chat"])
@@ -19,12 +20,11 @@ def create_room(payload: RoomCreate, db: Session = Depends(get_db), user: UserCl
def my_rooms(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
return ChatService(db).list_rooms_for_user(user.sub)
@router.get("/rooms/{room_id}", response_model=RoomRead)
def get_room(room_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
room = ChatService(db).get_room(room_id)
@router.get("/v1/rooms/{room_id}", response_model=RoomRead)
def get_room(room_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
room = RoomService(db).get(room_id, user.sub)
if not room:
raise HTTPException(status_code=404, detail="Not found")
# NOTE: для простоты опускаем проверку участия (добавьте в проде)
raise HTTPException(status_code=404, detail="Room not found")
return room
@router.post("/rooms/{room_id}/messages", response_model=MessageRead, status_code=201)
@@ -36,11 +36,8 @@ def send_message(room_id: str, payload: MessageCreate, db: Session = Depends(get
msg = svc.create_message(room_id, user.sub, payload.content)
return msg
@router.get("/rooms/{room_id}/messages", response_model=list[MessageRead])
def list_messages(room_id: str, offset: int = 0, limit: int = Query(100, le=500),
db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ChatService(db)
room = svc.get_room(room_id)
if not room:
@router.get("/v1/rooms/{room_id}/messages", response_model=list[MessageRead])
def list_messages(room_id: UUID, offset: int = 0, limit: int = 100, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
if not RoomService(db).exists(room_id, user.sub):
raise HTTPException(status_code=404, detail="Room not found")
return svc.list_messages(room_id, offset=offset, limit=limit)
return MessageService(db).list(room_id, user.sub, offset, limit)

View File

@@ -2,6 +2,8 @@ FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN mkdir -p /app/uploads
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .

View File

@@ -7,6 +7,9 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
RUN mkdir -p /app/uploads
RUN mkdir -p /app/uploads
COPY src ./src
COPY alembic.ini ./

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e
# Run migrations (no-op if no revisions yet)
alembic -c alembic.ini upgrade head || true
# Start app
exec uvicorn app.main:app --host 0.0.0.0 --port 8000

View File

@@ -1,5 +1,5 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.db.session import get_db
@@ -23,42 +23,33 @@ def list_pairs(for_user_id: str | None = None, status: str | None = None,
_: UserClaims = Depends(get_current_user)):
return PairService(db).list(for_user_id=for_user_id, status=status, offset=offset, limit=limit)
@router.get("/{pair_id}", response_model=PairRead)
def get_pair(pair_id: str, db: Session = Depends(get_db), _: UserClaims = Depends(get_current_user)):
obj = PairService(db).get(pair_id)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return obj
@router.get("/v1/pairs/{pair_id}", response_model=PairRead)
def get_pair(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
pair = PairService(db).get(pair_id)
if not pair:
raise HTTPException(status_code=404, detail="Pair not found")
return pair
@router.patch("/{pair_id}", response_model=PairRead)
def update_pair(pair_id: str, payload: PairUpdate, db: Session = Depends(get_db),
_: UserClaims = Depends(require_roles("ADMIN","MATCHMAKER"))):
svc = PairService(db)
obj = svc.get(pair_id)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return svc.update(obj, **payload.model_dump(exclude_none=True))
@router.patch("/v1/pairs/{pair_id}", response_model=PairRead)
def update_pair(pair_id: UUID, payload: PairUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(require_admin)):
updated = PairService(db).update(pair_id, payload)
if not updated:
raise HTTPException(status_code=404, detail="Pair not found")
return updated
@router.post("/{pair_id}/accept", response_model=PairRead)
def accept(pair_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = PairService(db)
obj = svc.get(pair_id)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
# Validate that current user participates
if user.sub not in (str(obj.user_id_a), str(obj.user_id_b)):
raise HTTPException(status_code=403, detail="Not allowed")
return svc.set_status(obj, "accepted")
@router.post("/v1/pairs/{pair_id}/accept", response_model=PairRead)
def accept(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
res = PairService(db).accept(pair_id, user.sub)
if not res:
raise HTTPException(status_code=404, detail="Pair not found")
return res
@router.post("/{pair_id}/reject", response_model=PairRead)
def reject(pair_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = PairService(db)
obj = svc.get(pair_id)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
if user.sub not in (str(obj.user_id_a), str(obj.user_id_b)):
raise HTTPException(status_code=403, detail="Not allowed")
return svc.set_status(obj, "rejected")
@router.post("/v1/pairs/{pair_id}/reject", response_model=PairRead)
def reject(pair_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
res = PairService(db).reject(pair_id, user.sub)
if not res:
raise HTTPException(status_code=404, detail="Pair not found")
return res
@router.delete("/{pair_id}", status_code=204)
def delete_pair(pair_id: str, db: Session = Depends(get_db),

View File

@@ -7,6 +7,8 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
RUN mkdir -p /app/uploads
COPY src ./src
COPY alembic.ini ./

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e
# Run migrations (no-op if no revisions yet)
alembic -c alembic.ini upgrade head || true
# Start app
exec uvicorn app.main:app --host 0.0.0.0 --port 8000

View File

@@ -7,6 +7,7 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
RUN mkdir -p /app/uploads
COPY src ./src
COPY alembic.ini ./

View File

@@ -0,0 +1,28 @@
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_profile_photo_and_likes'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# photo_url
with op.batch_alter_table('profiles', schema=None) as batch_op:
batch_op.add_column(sa.Column('photo_url', sa.String(), nullable=True))
# likes
op.create_table(
'profile_likes',
sa.Column('liker_user_id', sa.String(), primary_key=True),
sa.Column('target_user_id', sa.String(), primary_key=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
)
op.create_unique_constraint('uq_profile_like', 'profile_likes', ['liker_user_id', 'target_user_id'])
def downgrade():
op.drop_constraint('uq_profile_like', 'profile_likes', type_='unique')
op.drop_table('profile_likes')
with op.batch_alter_table('profiles', schema=None) as batch_op:
batch_op.drop_column('photo_url')

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e
# Run migrations (no-op if no revisions yet)
alembic -c alembic.ini upgrade head || true
# Start app
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level debug

View File

@@ -9,3 +9,4 @@ python-dotenv
httpx>=0.27
pytest
PyJWT>=2.8
python-multipart==0.0.9

View File

@@ -1,31 +1,143 @@
from fastapi import APIRouter, Depends, HTTPException, status
from __future__ import annotations
from typing import List, Optional
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from sqlalchemy.orm import Session
from ...core.security import get_current_user, require_roles, UserClaims
from ...db.session import get_db
from ...models.profile import Profile
from ...schemas.profile import ProfileCreate, ProfileUpdate, ProfileOut, LikesList
from ...services.profile_service import ProfileService
from ...services.profile_search_service import ProfileSearchService
from ...services.likes_service import LikesService
import os
from fastapi import status
from app.db.deps import get_db
from app.core.security import get_current_user, JwtUser
from app.schemas.profile import ProfileCreate, ProfileOut
from app.repositories.profile_repository import ProfileRepository
from app.services.profile_service import ProfileService
router = APIRouter(prefix="/v1/profiles", tags=["profiles"])
# отключаем авто-редирект /path -> /path/
router = APIRouter(prefix="/v1/profiles", tags=["profiles"], redirect_slashes=False)
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")
BASE_EXTERNAL_URL = os.getenv("BASE_EXTERNAL_URL", "http://localhost:8080")
@router.get("/me", response_model=ProfileOut)
def get_my_profile(current: JwtUser = Depends(get_current_user),
db: Session = Depends(get_db)):
svc = ProfileService(ProfileRepository(db))
p = svc.get_by_user(current.sub)
if not p:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found")
return p
def get_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.post("", response_model=ProfileOut, status_code=status.HTTP_201_CREATED)
def create_my_profile(payload: ProfileCreate,
current: JwtUser = Depends(get_current_user),
db: Session = Depends(get_db)):
svc = ProfileService(ProfileRepository(db))
existing = svc.get_by_user(current.sub)
if existing:
# если хотите строго — верните 409; оставлю 200/201 для удобства e2e
return existing
return svc.create(current.sub, payload)
@router.post("", response_model=ProfileOut, status_code=201)
def create_profile(payload: ProfileCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
if svc.get_by_user_id(user.sub):
raise HTTPException(status_code=409, detail="Profile already exists")
return svc.create(user.sub, **payload.model_dump())
@router.get("", response_model=List[ProfileOut])
def list_profiles(q: Optional[str] = None,
gender: Optional[str] = Query(None, pattern="^(male|female|other)$"),
city: Optional[str] = None,
languages: Optional[List[str]] = Query(None),
interests: Optional[List[str]] = Query(None),
has_photo: Optional[bool] = None,
sort_by: Optional[str] = Query(None, pattern="^(created_at|updated_at|city|gender)$"),
order: Optional[str] = Query("asc", pattern="^(asc|desc)$"),
offset: int = 0, limit: int = Query(50, le=200),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user)):
# ADMIN видит всех; CLIENT — тоже ok для MVP
svc = ProfileSearchService(db)
return svc.list_profiles(q=q, gender=gender, city=city,
languages=languages, interests=interests, has_photo=has_photo,
sort_by=sort_by, order=order, offset=offset, limit=limit)
@router.get("/v1/profiles/{profile_id}", response_model=ProfileOut)
def get_profile(profile_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
prof = ProfileService(db).get(profile_id)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.get("/v1/profiles/by-user/{user_id}", response_model=ProfileOut)
def get_by_user(user_id: UUID, db: Session = Depends(get_db), user: UserClaims = Depends(require_auth)):
prof = ProfileService(db).get_by_user_id(user_id)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.patch("/me", response_model=ProfileOut)
def patch_me(payload: ProfileUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
data = payload.model_dump(exclude_none=True)
return svc.update(prof, **data)
@router.delete("/me", status_code=204)
def delete_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
return
svc.delete(prof)
return
# === Photo upload ===
@router.post("/me/photo", response_model=ProfileOut)
def upload_photo(file: UploadFile = File(...),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if file.content_type not in ("image/jpeg", "image/png", "image/webp"):
raise HTTPException(status_code=400, detail="Invalid content-type")
os.makedirs(UPLOAD_DIR, exist_ok=True)
ext = { "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp" }[file.content_type]
subdir = os.path.join(UPLOAD_DIR, "avatars")
os.makedirs(subdir, exist_ok=True)
filename = f"{user.sub}.{ext}"
path = os.path.join(subdir, filename)
with open(path, "wb") as f:
f.write(file.file.read())
# URL через gateway: /profiles/static/avatars/<filename>
public_url = f"{BASE_EXTERNAL_URL}/profiles/static/avatars/{filename}"
return svc.update(prof, photo_url=public_url)
@router.delete("/me/photo", response_model=ProfileOut)
def delete_photo(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
return svc.update(prof, photo_url=None)
# === Likes ===
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
def _compat_likes_redirect():
# Заглушка для генераторов — реальный path ниже (/v1/likes)
return LikesList(items=[])
likes_router = APIRouter(prefix="/v1/likes", tags=["profiles"])
@likes_router.get("", response_model=LikesList)
def my_likes(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
items = LikesService(db).list_my_likes(user.sub)
return LikesList(items=items)
@likes_router.put("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def put_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).put_like(user.sub, target_user_id)
return
@likes_router.delete("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).delete_like(user.sub, target_user_id)
return
@likes_router.get("/mutual", response_model=List[str])
def mutual(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
return LikesService(db).mutual(user.sub)

View File

@@ -0,0 +1,31 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.deps import get_db
from app.core.security import get_current_user, JwtUser
from app.schemas.profile import ProfileCreate, ProfileOut
from app.repositories.profile_repository import ProfileRepository
from app.services.profile_service import ProfileService
# отключаем авто-редирект /path -> /path/
router = APIRouter(prefix="/v1/profiles", tags=["profiles"], redirect_slashes=False)
@router.get("/me", response_model=ProfileOut)
def get_my_profile(current: JwtUser = Depends(get_current_user),
db: Session = Depends(get_db)):
svc = ProfileService(ProfileRepository(db))
p = svc.get_by_user(current.sub)
if not p:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found")
return p
@router.post("", response_model=ProfileOut, status_code=status.HTTP_201_CREATED)
def create_my_profile(payload: ProfileCreate,
current: JwtUser = Depends(get_current_user),
db: Session = Depends(get_db)):
svc = ProfileService(ProfileRepository(db))
existing = svc.get_by_user(current.sub)
if existing:
# если хотите строго — верните 409; оставлю 200/201 для удобства e2e
return existing
return svc.create(current.sub, payload)

View File

@@ -0,0 +1,147 @@
from __future__ import annotations
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from sqlalchemy.orm import Session
from ....core.security import get_current_user, require_roles, UserClaims
from ....db.session import get_db
from ....models.profile import Profile
from ....schemas.profile import ProfileCreate, ProfileUpdate, ProfileOut, LikesList
from ....services.profile_service import ProfileService
from ....services.profile_search_service import ProfileSearchService
from ....services.likes_service import LikesService
import os
from fastapi import status
router = APIRouter(prefix="/v1/profiles", tags=["profiles"])
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")
BASE_EXTERNAL_URL = os.getenv("BASE_EXTERNAL_URL", "http://localhost:8080")
@router.get("/me", response_model=ProfileOut)
def get_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.post("", response_model=ProfileOut, status_code=201)
def create_profile(payload: ProfileCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
if svc.get_by_user_id(user.sub):
raise HTTPException(status_code=409, detail="Profile already exists")
return svc.create(user.sub, **payload.model_dump())
@router.get("", response_model=List[ProfileOut])
def list_profiles(q: Optional[str] = None,
gender: Optional[str] = Query(None, pattern="^(male|female|other)$"),
city: Optional[str] = None,
languages: Optional[List[str]] = Query(None),
interests: Optional[List[str]] = Query(None),
has_photo: Optional[bool] = None,
sort_by: Optional[str] = Query(None, pattern="^(created_at|updated_at|city|gender)$"),
order: Optional[str] = Query("asc", pattern="^(asc|desc)$"),
offset: int = 0, limit: int = Query(50, le=200),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user)):
# ADMIN видит всех; CLIENT — тоже ok для MVP
svc = ProfileSearchService(db)
return svc.list_profiles(q=q, gender=gender, city=city,
languages=languages, interests=interests, has_photo=has_photo,
sort_by=sort_by, order=order, offset=offset, limit=limit)
@router.get("/{profile_id}", response_model=ProfileOut)
def get_profile(profile_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get(profile_id)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if user.role != "ADMIN" and prof.user_id != user.sub:
# при необходимости можно сделать публичным — тут ограничение на владение/admin
pass
return prof
@router.get("/by-user/{user_id}", response_model=ProfileOut)
def get_by_user(user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user_id)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if user.role != "ADMIN" and user_id != user.sub:
pass
return prof
@router.patch("/me", response_model=ProfileOut)
def patch_me(payload: ProfileUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
data = payload.model_dump(exclude_none=True)
return svc.update(prof, **data)
@router.delete("/me", status_code=204)
def delete_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
return
svc.delete(prof)
return
# === Photo upload ===
@router.post("/me/photo", response_model=ProfileOut)
def upload_photo(file: UploadFile = File(...),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if file.content_type not in ("image/jpeg", "image/png", "image/webp"):
raise HTTPException(status_code=400, detail="Invalid content-type")
os.makedirs(UPLOAD_DIR, exist_ok=True)
ext = { "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp" }[file.content_type]
subdir = os.path.join(UPLOAD_DIR, "avatars")
os.makedirs(subdir, exist_ok=True)
filename = f"{user.sub}.{ext}"
path = os.path.join(subdir, filename)
with open(path, "wb") as f:
f.write(file.file.read())
# URL через gateway: /profiles/static/avatars/<filename>
public_url = f"{BASE_EXTERNAL_URL}/profiles/static/avatars/{filename}"
return svc.update(prof, photo_url=public_url)
@router.delete("/me/photo", response_model=ProfileOut)
def delete_photo(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
return svc.update(prof, photo_url=None)
# === Likes ===
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
def _compat_likes_redirect():
# Заглушка для генераторов — реальный path ниже (/v1/likes)
return LikesList(items=[])
likes_router = APIRouter(prefix="/v1/likes", tags=["profiles"])
@likes_router.get("", response_model=LikesList)
def my_likes(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
items = LikesService(db).list_my_likes(user.sub)
return LikesList(items=items)
@likes_router.put("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def put_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).put_like(user.sub, target_user_id)
return
@likes_router.delete("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).delete_like(user.sub, target_user_id)
return
@likes_router.get("/mutual", response_model=List[str])
def mutual(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
return LikesService(db).mutual(user.sub)

View File

@@ -0,0 +1,147 @@
from __future__ import annotations
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from sqlalchemy.orm import Session
from ...core.security import get_current_user, require_roles, UserClaims
from ...db.session import get_db
from ...models.profile import Profile
from ...schemas.profile import ProfileCreate, ProfileUpdate, ProfileOut, LikesList
from ....services.profile_service import ProfileService
from ....services.profile_search_service import ProfileSearchService
from ....services.likes_service import LikesService
import os
from fastapi import status
router = APIRouter(prefix="/v1/profiles", tags=["profiles"])
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")
BASE_EXTERNAL_URL = os.getenv("BASE_EXTERNAL_URL", "http://localhost:8080")
@router.get("/me", response_model=ProfileOut)
def get_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Profile not found")
return prof
@router.post("", response_model=ProfileOut, status_code=201)
def create_profile(payload: ProfileCreate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
if svc.get_by_user_id(user.sub):
raise HTTPException(status_code=409, detail="Profile already exists")
return svc.create(user.sub, **payload.model_dump())
@router.get("", response_model=List[ProfileOut])
def list_profiles(q: Optional[str] = None,
gender: Optional[str] = Query(None, pattern="^(male|female|other)$"),
city: Optional[str] = None,
languages: Optional[List[str]] = Query(None),
interests: Optional[List[str]] = Query(None),
has_photo: Optional[bool] = None,
sort_by: Optional[str] = Query(None, pattern="^(created_at|updated_at|city|gender)$"),
order: Optional[str] = Query("asc", pattern="^(asc|desc)$"),
offset: int = 0, limit: int = Query(50, le=200),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user)):
# ADMIN видит всех; CLIENT — тоже ok для MVP
svc = ProfileSearchService(db)
return svc.list_profiles(q=q, gender=gender, city=city,
languages=languages, interests=interests, has_photo=has_photo,
sort_by=sort_by, order=order, offset=offset, limit=limit)
@router.get("/{profile_id}", response_model=ProfileOut)
def get_profile(profile_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get(profile_id)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if user.role != "ADMIN" and prof.user_id != user.sub:
# при необходимости можно сделать публичным — тут ограничение на владение/admin
pass
return prof
@router.get("/by-user/{user_id}", response_model=ProfileOut)
def get_by_user(user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
prof = ProfileService(db).get_by_user_id(user_id)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if user.role != "ADMIN" and user_id != user.sub:
pass
return prof
@router.patch("/me", response_model=ProfileOut)
def patch_me(payload: ProfileUpdate, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
data = payload.model_dump(exclude_none=True)
return svc.update(prof, **data)
@router.delete("/me", status_code=204)
def delete_me(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
return
svc.delete(prof)
return
# === Photo upload ===
@router.post("/me/photo", response_model=ProfileOut)
def upload_photo(file: UploadFile = File(...),
db: Session = Depends(get_db),
user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
if file.content_type not in ("image/jpeg", "image/png", "image/webp"):
raise HTTPException(status_code=400, detail="Invalid content-type")
os.makedirs(UPLOAD_DIR, exist_ok=True)
ext = { "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp" }[file.content_type]
subdir = os.path.join(UPLOAD_DIR, "avatars")
os.makedirs(subdir, exist_ok=True)
filename = f"{user.sub}.{ext}"
path = os.path.join(subdir, filename)
with open(path, "wb") as f:
f.write(file.file.read())
# URL через gateway: /profiles/static/avatars/<filename>
public_url = f"{BASE_EXTERNAL_URL}/profiles/static/avatars/{filename}"
return svc.update(prof, photo_url=public_url)
@router.delete("/me/photo", response_model=ProfileOut)
def delete_photo(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
svc = ProfileService(db)
prof = svc.get_by_user_id(user.sub)
if not prof:
raise HTTPException(status_code=404, detail="Not found")
return svc.update(prof, photo_url=None)
# === Likes ===
@router.get("/../likes", response_model=LikesList, include_in_schema=False)
def _compat_likes_redirect():
# Заглушка для генераторов — реальный path ниже (/v1/likes)
return LikesList(items=[])
likes_router = APIRouter(prefix="/v1/likes", tags=["profiles"])
@likes_router.get("", response_model=LikesList)
def my_likes(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
items = LikesService(db).list_my_likes(user.sub)
return LikesList(items=items)
@likes_router.put("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def put_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).put_like(user.sub, target_user_id)
return
@likes_router.delete("/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_like(target_user_id: str, db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
LikesService(db).delete_like(user.sub, target_user_id)
return
@likes_router.get("/mutual", response_model=List[str])
def mutual(db: Session = Depends(get_db), user: UserClaims = Depends(get_current_user)):
return LikesService(db).mutual(user.sub)

View File

@@ -57,3 +57,64 @@ def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(reusabl
if credentials.scheme.lower() != "bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
return decode_token(credentials.credentials)
# --- added by patch: role-based dependency ---
from fastapi import Depends, HTTPException, status # noqa: E402
def require_roles(*roles: str):
"""
FastAPI dependency: ensure current user has one of the roles.
Usage: Depends(require_roles("ADMIN", "MATCHMAKER"))
"""
def _dep(user: "UserClaims" = Depends(get_current_user)):
if not getattr(user, "role", None) in roles:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return user
return _dep
# --- end patch ---
# --- added by patch: UserClaims + normalization ---
from typing import Optional, Any # noqa: E402
from pydantic import BaseModel # noqa: E402
class UserClaims(BaseModel):
sub: str
role: str
email: Optional[str] = None
def _as_user_claims(u: Any) -> "UserClaims":
"""Приводит произвольный объект пользователя к UserClaims."""
if isinstance(u, UserClaims):
return u
if isinstance(u, dict):
return UserClaims(**u)
if hasattr(u, "model_dump"): # pydantic v2
return UserClaims(**u.model_dump())
# на крайний случай — достанем атрибуты
return UserClaims(
sub=str(getattr(u, "sub", "")),
role=str(getattr(u, "role", "")),
email=getattr(u, "email", None),
)
# если есть require_roles — заставим его использовать нормализацию
try:
# find the already-patched require_roles and wrap its inner dep
import inspect
if 'require_roles' in globals():
_orig_require_roles = require_roles
def require_roles(*roles: str): # type: ignore[override]
dep = _orig_require_roles(*roles)
def _wrapped(user = Depends(get_current_user)): # noqa: F821
u = _as_user_claims(user)
if u.role not in roles:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return u
# Вернём совместимый Depends
from fastapi import Depends
return _wrapped
except Exception:
# тихо игнорируем — значит require_roles ещё не определён, всё ок
pass
# --- end patch ---

View File

@@ -0,0 +1,59 @@
import os
from typing import Optional
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
reusable_bearer = HTTPBearer(auto_error=True)
JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
# Возможность включить строгую проверку audience/issuer в будущем
JWT_VERIFY_AUD = os.getenv("JWT_VERIFY_AUD", "0") == "1"
JWT_AUDIENCE: Optional[str] = os.getenv("JWT_AUDIENCE") or None
JWT_VERIFY_ISS = os.getenv("JWT_VERIFY_ISS", "0") == "1"
JWT_ISSUER: Optional[str] = os.getenv("JWT_ISSUER") or None
# Допустимая рассинхронизация часов (сек)
JWT_LEEWAY = int(os.getenv("JWT_LEEWAY", "30"))
class JwtUser(BaseModel):
sub: str
email: Optional[str] = None
role: Optional[str] = None
def decode_token(token: str) -> JwtUser:
options = {
"verify_signature": True,
"verify_exp": True,
"verify_aud": JWT_VERIFY_AUD,
"verify_iss": JWT_VERIFY_ISS,
}
kwargs = {"algorithms": [JWT_ALGORITHM], "options": options, "leeway": JWT_LEEWAY}
if JWT_VERIFY_AUD and JWT_AUDIENCE:
kwargs["audience"] = JWT_AUDIENCE
if JWT_VERIFY_ISS and JWT_ISSUER:
kwargs["issuer"] = JWT_ISSUER
try:
payload = jwt.decode(token, JWT_SECRET, **kwargs)
sub = str(payload.get("sub") or "")
if not sub:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: no sub")
return JwtUser(sub=sub, email=payload.get("email"), role=payload.get("role"))
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except jwt.InvalidAudienceError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid audience")
except jwt.InvalidIssuerError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid issuer")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(reusable_bearer)) -> JwtUser:
if credentials.scheme.lower() != "bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
return decode_token(credentials.credentials)

View File

@@ -0,0 +1,74 @@
import os
from typing import Optional
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
reusable_bearer = HTTPBearer(auto_error=True)
JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
# Возможность включить строгую проверку audience/issuer в будущем
JWT_VERIFY_AUD = os.getenv("JWT_VERIFY_AUD", "0") == "1"
JWT_AUDIENCE: Optional[str] = os.getenv("JWT_AUDIENCE") or None
JWT_VERIFY_ISS = os.getenv("JWT_VERIFY_ISS", "0") == "1"
JWT_ISSUER: Optional[str] = os.getenv("JWT_ISSUER") or None
# Допустимая рассинхронизация часов (сек)
JWT_LEEWAY = int(os.getenv("JWT_LEEWAY", "30"))
class JwtUser(BaseModel):
sub: str
email: Optional[str] = None
role: Optional[str] = None
def decode_token(token: str) -> JwtUser:
options = {
"verify_signature": True,
"verify_exp": True,
"verify_aud": JWT_VERIFY_AUD,
"verify_iss": JWT_VERIFY_ISS,
}
kwargs = {"algorithms": [JWT_ALGORITHM], "options": options, "leeway": JWT_LEEWAY}
if JWT_VERIFY_AUD and JWT_AUDIENCE:
kwargs["audience"] = JWT_AUDIENCE
if JWT_VERIFY_ISS and JWT_ISSUER:
kwargs["issuer"] = JWT_ISSUER
try:
payload = jwt.decode(token, JWT_SECRET, **kwargs)
sub = str(payload.get("sub") or "")
if not sub:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: no sub")
return JwtUser(sub=sub, email=payload.get("email"), role=payload.get("role"))
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except jwt.InvalidAudienceError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid audience")
except jwt.InvalidIssuerError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid issuer")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(reusable_bearer)) -> JwtUser:
if credentials.scheme.lower() != "bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid auth scheme")
return decode_token(credentials.credentials)
# --- added by patch: role-based dependency ---
from fastapi import Depends, HTTPException, status # noqa: E402
def require_roles(*roles: str):
"""
FastAPI dependency: ensure current user has one of the roles.
Usage: Depends(require_roles("ADMIN", "MATCHMAKER"))
"""
def _dep(user: "UserClaims" = Depends(get_current_user)):
if not getattr(user, "role", None) in roles:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return user
return _dep
# --- end patch ---

View File

@@ -1,12 +1,17 @@
from fastapi import FastAPI
from starlette.staticfiles import StaticFiles
from .api.routes.ping import router as ping_router
from .api.routes.profiles import router as profiles_router
from .api.routes.profiles import router as profiles_router, likes_router as profiles_likes_router
app = FastAPI(title="PROFILES Service")
app.mount("/static", StaticFiles(directory="/app/uploads"), name="static")
@app.get("/health")
def health():
return {"status": "ok", "service": "profiles"}
app.include_router(ping_router, prefix="/v1")
app.include_router(profiles_router)
app.include_router(profiles_likes_router)

View File

@@ -0,0 +1,12 @@
from datetime import datetime
from sqlalchemy import Column, String, DateTime, UniqueConstraint
from .base import Base
class ProfileLike(Base):
__tablename__ = "profile_likes"
liker_user_id = Column(String, primary_key=True) # UUID (as str)
target_user_id = Column(String, primary_key=True) # UUID (as str)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
__table_args__ = (
UniqueConstraint('liker_user_id', 'target_user_id', name='uq_profile_like'),
)

View File

@@ -0,0 +1,37 @@
from typing import List
from sqlalchemy.orm import Session
from ..models.like import ProfileLike
class LikesRepository:
def __init__(self, db: Session):
self.db = db
def list_my_likes(self, user_id: str) -> List[str]:
rows = self.db.query(ProfileLike).filter(ProfileLike.liker_user_id == user_id).all()
return [r.target_user_id for r in rows]
def put_like(self, liker: str, target: str) -> None:
exists = self.db.query(ProfileLike).filter(
ProfileLike.liker_user_id == liker,
ProfileLike.target_user_id == target
).first()
if exists:
return
self.db.add(ProfileLike(liker_user_id=liker, target_user_id=target))
self.db.commit()
def delete_like(self, liker: str, target: str) -> None:
self.db.query(ProfileLike).filter(
ProfileLike.liker_user_id == liker,
ProfileLike.target_user_id == target
).delete()
self.db.commit()
def mutual_likes(self, user_id: str) -> List[str]:
# users that user_id likes AND who like user_id back
sub = self.db.query(ProfileLike.target_user_id).filter(ProfileLike.liker_user_id == user_id).subquery()
rows = self.db.query(ProfileLike.liker_user_id).filter(
ProfileLike.target_user_id == user_id,
ProfileLike.liker_user_id.in_(sub)
).all()
return [r[0] for r in rows]

View File

@@ -1,26 +1,81 @@
from typing import Optional
# services/profiles/src/app/repositories/profile_repository.py
from __future__ import annotations
from typing import List, Optional
from uuid import UUID
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.models.profile import Profile
from app.schemas.profile import ProfileCreate
class ProfileRepository:
def __init__(self, db: Session):
self.db = db
def get_by_user(self, user_id: UUID) -> Optional[Profile]:
return self.db.execute(select(Profile).where(Profile.user_id == user_id)).scalar_one_or_none()
def create(self, user_id: UUID, data: ProfileCreate) -> Profile:
p = Profile(
user_id=user_id,
gender=data.gender,
city=data.city,
languages=list(data.languages or []),
interests=list(data.interests or []),
def get_by_user_id(self, user_id: UUID) -> Optional[Profile]:
return (
self.db.query(Profile)
.filter(Profile.user_id == user_id)
.first()
)
self.db.add(p)
# оставляем старое имя для обратной совместимости
def get_by_user(self, user_id: UUID) -> Optional[Profile]:
return self.get_by_user_id(user_id)
def get_by_id(self, profile_id: UUID) -> Optional[Profile]:
return self.db.query(Profile).filter(Profile.id == profile_id).first()
def create(
self,
*,
user_id: UUID,
gender: str,
city: str,
languages: List[str],
interests: List[str],
photo_url: Optional[str] = None,
) -> Profile:
# не передаём photo_url в конструктор — модели может не быть этого поля
obj = Profile(
user_id=user_id,
gender=gender,
city=city,
languages=languages,
interests=interests,
)
# выставим только если поле реально существует
if photo_url is not None and hasattr(obj, "photo_url"):
setattr(obj, "photo_url", photo_url)
self.db.add(obj)
self.db.commit()
self.db.refresh(p)
return p
self.db.refresh(obj)
return obj
def update_me(
self,
*,
profile: Profile,
gender: Optional[str] = None,
city: Optional[str] = None,
languages: Optional[List[str]] = None,
interests: Optional[List[str]] = None,
photo_url: Optional[Optional[str]] = None, # None — «не менять», явный null — «стереть»
) -> Profile:
if gender is not None:
profile.gender = gender
if city is not None:
profile.city = city
if languages is not None:
profile.languages = languages
if interests is not None:
profile.interests = interests
if photo_url is not None and hasattr(profile, "photo_url"):
# сюда придёт либо строка (установить), либо явный None (стереть)
setattr(profile, "photo_url", photo_url)
self.db.commit()
self.db.refresh(profile)
return profile

View File

@@ -0,0 +1,26 @@
from typing import Optional
from uuid import UUID
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.models.profile import Profile
from app.schemas.profile import ProfileCreate
class ProfileRepository:
def __init__(self, db: Session):
self.db = db
def get_by_user(self, user_id: UUID) -> Optional[Profile]:
return self.db.execute(select(Profile).where(Profile.user_id == user_id)).scalar_one_or_none()
def create(self, user_id: UUID, data: ProfileCreate) -> Profile:
p = Profile(
user_id=user_id,
gender=data.gender,
city=data.city,
languages=list(data.languages or []),
interests=list(data.interests or []),
)
self.db.add(p)
self.db.commit()
self.db.refresh(p)
return p

View File

@@ -0,0 +1,55 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import or_, func, text
from ..models.profile import Profile
class ProfileSearchRepository:
def __init__(self, db: Session):
self.db = db
def list_profiles(self,
q: Optional[str],
gender: Optional[str],
city: Optional[str],
languages: Optional[List[str]],
interests: Optional[List[str]],
has_photo: Optional[bool],
sort_by: Optional[str],
order: Optional[str],
offset: int, limit: int) -> List[Profile]:
query = self.db.query(Profile)
if q:
ilike = f"%{q}%"
query = query.filter(
or_(Profile.city.ilike(ilike),
func.cast(Profile.languages, text('TEXT')).ilike(ilike),
func.cast(Profile.interests, text('TEXT')).ilike(ilike))
)
if gender:
query = query.filter(Profile.gender == gender)
if city:
query = query.filter(Profile.city.ilike(city) | (Profile.city == city))
if languages:
# пересечение хотя бы по одному
for l in languages:
query = query.filter(func.cast(Profile.languages, text('TEXT')).ilike(f'%"{l}"%'))
if interests:
for i in interests:
query = query.filter(func.cast(Profile.interests, text('TEXT')).ilike(f'%"{i}"%'))
if has_photo is True:
query = query.filter(Profile.photo_url.isnot(None))
if has_photo is False:
query = query.filter(Profile.photo_url.is_(None))
sort_map = {
"created_at": Profile.created_at if hasattr(Profile, "created_at") else None,
"updated_at": Profile.updated_at if hasattr(Profile, "updated_at") else None,
"city": Profile.city,
"gender": Profile.gender,
}
col = sort_map.get(sort_by or "", Profile.created_at if hasattr(Profile, "created_at") else Profile.id)
if order == "desc":
col = col.desc()
query = query.order_by(col)
return query.offset(offset).limit(min(limit, 200)).all()

View File

@@ -1,32 +1,77 @@
# from pydantic import BaseModel, Field, HttpUrl
# from typing import Optional, List, Literal
# from uuid import UUID
# class ProfileCreate(BaseModel):
# gender: Literal["male", "female", "other"]
# city: str
# languages: List[str] = []
# interests: List[str] = []
# class ProfileUpdate(BaseModel):
# gender: Optional[Literal["male", "female", "other"]] = None
# city: Optional[str] = None
# languages: Optional[List[str]] = None
# interests: Optional[List[str]] = None
# photo_url: Optional[Optional[str]] = Field(default=None)
# class ProfileOut(BaseModel):
# id: UUID
# user_id: UUID
# gender: Literal["male", "female", "other"]
# city: str
# languages: List[str] = []
# interests: List[str] = []
# photo_url: Optional[str] = None
# from pydantic import ConfigDict
# model_config = ConfigDict(from_attributes=True)
# class LikesList(BaseModel):
# items: List[str]
# services/profiles/src/app/schemas/profile.py
from __future__ import annotations
from typing import List, Optional
from uuid import UUID
from typing import List
try:
# Pydantic v2
from pydantic import BaseModel, Field, ConfigDict
_V2 = True
except Exception:
# Pydantic v1 fallback
from pydantic import BaseModel, Field
ConfigDict = None
_V2 = False
from pydantic import BaseModel, ConfigDict
# Базовые поля профиля
class ProfileBase(BaseModel):
gender: str
city: str
languages: List[str] = Field(default_factory=list)
interests: List[str] = Field(default_factory=list)
languages: List[str]
interests: List[str]
# Для POST /profiles/v1/profiles
class ProfileCreate(ProfileBase):
pass
# Для PATCH /profiles/v1/profiles/me (все поля опциональные)
class ProfileUpdate(BaseModel):
gender: Optional[str] = None
city: Optional[str] = None
languages: Optional[List[str]] = None
interests: Optional[List[str]] = None
photo_url: Optional[str] = None # nullable
# Для ответов
class ProfileOut(ProfileBase):
id: UUID
user_id: UUID
photo_url: Optional[str] = None
if _V2:
# Важно: для возврата ORM-объектов
model_config = ConfigDict(from_attributes=True)
else:
class Config:
orm_mode = True
# Для лайков
class LikesList(BaseModel):
items: List[UUID]

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from uuid import UUID
from typing import List
try:
# Pydantic v2
from pydantic import BaseModel, Field, ConfigDict
_V2 = True
except Exception:
# Pydantic v1 fallback
from pydantic import BaseModel, Field
ConfigDict = None
_V2 = False
class ProfileBase(BaseModel):
gender: str
city: str
languages: List[str] = Field(default_factory=list)
interests: List[str] = Field(default_factory=list)
class ProfileCreate(ProfileBase):
pass
class ProfileOut(ProfileBase):
id: UUID
user_id: UUID
if _V2:
model_config = ConfigDict(from_attributes=True)
else:
class Config:
orm_mode = True

View File

@@ -0,0 +1,27 @@
from pydantic import BaseModel, Field, HttpUrl
from typing import Optional, List, Literal
class ProfileCreate(BaseModel):
gender: Literal["male", "female", "other"]
city: str
languages: List[str] = []
interests: List[str] = []
class ProfileUpdate(BaseModel):
gender: Optional[Literal["male", "female", "other"]] = None
city: Optional[str] = None
languages: Optional[List[str]] = None
interests: Optional[List[str]] = None
photo_url: Optional[Optional[str]] = Field(default=None)
class ProfileOut(BaseModel):
id: str
user_id: str
gender: Literal["male", "female", "other"]
city: str
languages: List[str] = []
interests: List[str] = []
photo_url: Optional[str] = None
class LikesList(BaseModel):
items: List[str]

View File

@@ -0,0 +1,21 @@
from typing import List
from sqlalchemy.orm import Session
from ..repositories.likes_repository import LikesRepository
class LikesService:
def __init__(self, db: Session):
self.repo = LikesRepository(db)
def list_my_likes(self, user_id: str) -> List[str]:
return self.repo.list_my_likes(user_id)
def put_like(self, liker: str, target: str) -> None:
if liker == target:
return
self.repo.put_like(liker, target)
def delete_like(self, liker: str, target: str) -> None:
self.repo.delete_like(liker, target)
def mutual(self, user_id: str) -> List[str]:
return self.repo.mutual_likes(user_id)

View File

@@ -0,0 +1,11 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from ..models.profile import Profile
from ..repositories.profile_search_repository import ProfileSearchRepository
class ProfileSearchService:
def __init__(self, db: Session):
self.repo = ProfileSearchRepository(db)
def list_profiles(self, **kwargs) -> List[Profile]:
return self.repo.list_profiles(**kwargs)

View File

@@ -1,13 +1,23 @@
from uuid import UUID
from app.schemas.profile import ProfileCreate
from app.repositories.profile_repository import ProfileRepository
from typing import Optional
from sqlalchemy.orm import Session
from ..models.profile import Profile
from ..repositories.profile_repository import ProfileRepository
class ProfileService:
def __init__(self, repo: ProfileRepository):
self.repo = repo
def __init__(self, db: Session):
self.repo = ProfileRepository(db)
def get_by_user(self, user_id: UUID):
return self.repo.get_by_user(user_id)
def get(self, profile_id: str) -> Optional[Profile]:
return self.repo.get(profile_id)
def create(self, user_id: UUID, data: ProfileCreate):
return self.repo.create(user_id, data)
def get_by_user_id(self, user_id: str) -> Optional[Profile]:
return self.repo.get_by_user_id(user_id)
def create(self, user_id: str, **data) -> Profile:
return self.repo.create(user_id=user_id, **data)
def update(self, prof: Profile, **data) -> Profile:
return self.repo.update(prof, **data)
def delete(self, prof: Profile) -> None:
return self.repo.delete(prof)

View File

@@ -0,0 +1,13 @@
from uuid import UUID
from app.schemas.profile import ProfileCreate
from app.repositories.profile_repository import ProfileRepository
class ProfileService:
def __init__(self, repo: ProfileRepository):
self.repo = repo
def get_by_user(self, user_id: UUID):
return self.repo.get_by_user(user_id)
def create(self, user_id: UUID, data: ProfileCreate):
return self.repo.create(user_id, data)