diff --git a/.gitignore b/.gitignore index d1d5c6c..d3749fd 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ build/ # IDE / OS .DS_Store -.idea/ +.idea/a .vscode/ # Docker diff --git a/chat/src/app/api/chat.py b/chat/src/app/api/chat.py new file mode 100644 index 0000000..d9b6e38 --- /dev/null +++ b/chat/src/app/api/chat.py @@ -0,0 +1 @@ +'"$@"' diff --git a/logs/audit_20250810_145217.log b/logs/audit_20250810_145217.log new file mode 100644 index 0000000..18aa828 --- /dev/null +++ b/logs/audit_20250810_145217.log @@ -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 path‑parameters 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… diff --git a/logs/audit_20250810_145252.json b/logs/audit_20250810_145252.json new file mode 100644 index 0000000..76e4145 --- /dev/null +++ b/logs/audit_20250810_145252.json @@ -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": [] + } + ] + } + ] + } +} diff --git a/logs/audit_20250810_145252.log b/logs/audit_20250810_145252.log new file mode 100644 index 0000000..57b1781 --- /dev/null +++ b/logs/audit_20250810_145252.log @@ -0,0 +1,445 @@ +[14:52:52] OpenAPI: http://localhost:8080/openapi.json +[14:52:52] ✔ OpenAPI fetched (32448 bytes) +[14:52:52] Analyze path‑parameters 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 diff --git a/logs/audit_20250810_145805.json b/logs/audit_20250810_145805.json new file mode 100644 index 0000000..6c6af23 --- /dev/null +++ b/logs/audit_20250810_145805.json @@ -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": [] + } + ] + } + ] + } +} diff --git a/logs/audit_20250810_145805.log b/logs/audit_20250810_145805.log new file mode 100644 index 0000000..1082ed0 --- /dev/null +++ b/logs/audit_20250810_145805.log @@ -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 path‑parameters 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 diff --git a/logs/audit_20250810_150332.json b/logs/audit_20250810_150332.json new file mode 100644 index 0000000..0648f94 --- /dev/null +++ b/logs/audit_20250810_150332.json @@ -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": [] + } + ] + } + ] + } +} diff --git a/logs/audit_20250810_150332.log b/logs/audit_20250810_150332.log new file mode 100644 index 0000000..11cab90 --- /dev/null +++ b/logs/audit_20250810_150332.log @@ -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 path‑parameters 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 diff --git a/logs/audit_20250810_150348.json b/logs/audit_20250810_150348.json new file mode 100644 index 0000000..a9f81ef --- /dev/null +++ b/logs/audit_20250810_150348.json @@ -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": [] + } + ] + } + ] + } +} diff --git a/logs/audit_20250810_150348.log b/logs/audit_20250810_150348.log new file mode 100644 index 0000000..c37b12c --- /dev/null +++ b/logs/audit_20250810_150348.log @@ -0,0 +1,445 @@ +[15:03:48] OpenAPI: http://localhost:8080/openapi.json +[15:03:48] ✔ OpenAPI fetched (32448 bytes) +[15:03:48] Analyze path‑parameters 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 diff --git a/logs/audit_20250810_153547.json b/logs/audit_20250810_153547.json new file mode 100644 index 0000000..914dda8 --- /dev/null +++ b/logs/audit_20250810_153547.json @@ -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": [] + } +} diff --git a/logs/audit_20250810_153547.log b/logs/audit_20250810_153547.log new file mode 100644 index 0000000..ae4ba6a --- /dev/null +++ b/logs/audit_20250810_153547.log @@ -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 path‑parameters in schema… +[15:35:48] ✔ Проблем с path‑параметрами в схеме не найдено. +[15:35:48] Report: logs/audit_20250810_153547.json +[15:35:48] Log: logs/audit_20250810_153547.log diff --git a/logs/openapi_audit_20250810_143846.log b/logs/openapi_audit_20250810_143846.log new file mode 100644 index 0000000..8b008ce --- /dev/null +++ b/logs/openapi_audit_20250810_143846.log @@ -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 diff --git a/logs/openapi_audit_20250810_143930.log b/logs/openapi_audit_20250810_143930.log new file mode 100644 index 0000000..60b166c --- /dev/null +++ b/logs/openapi_audit_20250810_143930.log @@ -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 diff --git a/logs/openapi_audit_20250810_143952.log b/logs/openapi_audit_20250810_143952.log new file mode 100644 index 0000000..1250b80 --- /dev/null +++ b/logs/openapi_audit_20250810_143952.log @@ -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 diff --git a/logs/openapi_audit_20250810_144126.log b/logs/openapi_audit_20250810_144126.log new file mode 100644 index 0000000..e3ca6a9 --- /dev/null +++ b/logs/openapi_audit_20250810_144126.log @@ -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. diff --git a/logs/openapi_audit_20250810_144251.log b/logs/openapi_audit_20250810_144251.log new file mode 100644 index 0000000..2df77c1 --- /dev/null +++ b/logs/openapi_audit_20250810_144251.log @@ -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 diff --git a/logs/openapi_audit_20250810_144444.log b/logs/openapi_audit_20250810_144444.log new file mode 100644 index 0000000..09bdc04 --- /dev/null +++ b/logs/openapi_audit_20250810_144444.log @@ -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 diff --git a/logs/openapi_audit_20250810_144749.log b/logs/openapi_audit_20250810_144749.log new file mode 100644 index 0000000..9890b24 --- /dev/null +++ b/logs/openapi_audit_20250810_144749.log @@ -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 diff --git a/logs/openapi_audit_20250810_144935.log b/logs/openapi_audit_20250810_144935.log new file mode 100644 index 0000000..e156d33 --- /dev/null +++ b/logs/openapi_audit_20250810_144935.log @@ -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 diff --git a/logs/openapi_audit_20250810_153553.log b/logs/openapi_audit_20250810_153553.log new file mode 100644 index 0000000..fc555ab --- /dev/null +++ b/logs/openapi_audit_20250810_153553.log @@ -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 diff --git a/logs/user_flow_20250810_142009.log b/logs/user_flow_20250810_142009.log new file mode 100644 index 0000000..dd87acd --- /dev/null +++ b/logs/user_flow_20250810_142009.log @@ -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 diff --git a/match/src/app/api/routes/pairs.py b/match/src/app/api/routes/pairs.py new file mode 100644 index 0000000..d9b6e38 --- /dev/null +++ b/match/src/app/api/routes/pairs.py @@ -0,0 +1 @@ +'"$@"' diff --git a/profiles/src/app/api/routes/profiles.py b/profiles/src/app/api/routes/profiles.py new file mode 100644 index 0000000..d9b6e38 --- /dev/null +++ b/profiles/src/app/api/routes/profiles.py @@ -0,0 +1 @@ +'"$@"' diff --git a/scripts/patch.sh b/scripts/patch.sh index 37c0871..ff3ca63 100755 --- a/scripts/patch.sh +++ b/scripts/patch.sh @@ -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" diff --git a/scripts/test.sh b/scripts/test.sh index e707b02..578785f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -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_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}" diff --git a/services/auth/Dockerfile b/services/auth/Dockerfile index 57c86d2..831bed9 100644 --- a/services/auth/Dockerfile +++ b/services/auth/Dockerfile @@ -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 ./ diff --git a/services/auth/docker-entrypoint.sh.bak.1754801031 b/services/auth/docker-entrypoint.sh.bak.1754801031 new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/auth/docker-entrypoint.sh.bak.1754801031 @@ -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 diff --git a/services/auth/src/app/api/routes/users_search.py b/services/auth/src/app/api/routes/users_search.py new file mode 100644 index 0000000..ef898aa --- /dev/null +++ b/services/auth/src/app/api/routes/users_search.py @@ -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) diff --git a/services/auth/src/app/main.py.bak.1754798399 b/services/auth/src/app/main.py.bak.1754798399 new file mode 100644 index 0000000..0c112ef --- /dev/null +++ b/services/auth/src/app/main.py.bak.1754798399 @@ -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) diff --git a/services/auth/src/app/repositories/user_search_repository.py b/services/auth/src/app/repositories/user_search_repository.py new file mode 100644 index 0000000..a95b4ee --- /dev/null +++ b/services/auth/src/app/repositories/user_search_repository.py @@ -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() diff --git a/services/chat/Dockerfile b/services/chat/Dockerfile index 57c86d2..831bed9 100644 --- a/services/chat/Dockerfile +++ b/services/chat/Dockerfile @@ -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 ./ diff --git a/services/chat/docker-entrypoint.sh.bak.1754801031 b/services/chat/docker-entrypoint.sh.bak.1754801031 new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/chat/docker-entrypoint.sh.bak.1754801031 @@ -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 diff --git a/services/chat/src/app/api/routes/chat.py b/services/chat/src/app/api/routes/chat.py index ea4a481..53f16e6 100644 --- a/services/chat/src/app/api/routes/chat.py +++ b/services/chat/src/app/api/routes/chat.py @@ -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) diff --git a/services/docs/Dockerfile b/services/docs/Dockerfile index f676143..085ffaf 100644 --- a/services/docs/Dockerfile +++ b/services/docs/Dockerfile @@ -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 . diff --git a/services/match/Dockerfile b/services/match/Dockerfile index 57c86d2..840e225 100644 --- a/services/match/Dockerfile +++ b/services/match/Dockerfile @@ -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 ./ diff --git a/services/match/docker-entrypoint.sh.bak.1754801031 b/services/match/docker-entrypoint.sh.bak.1754801031 new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/match/docker-entrypoint.sh.bak.1754801031 @@ -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 diff --git a/services/match/src/app/api/routes/pairs.py b/services/match/src/app/api/routes/pairs.py index a2e8053..95e4eab 100644 --- a/services/match/src/app/api/routes/pairs.py +++ b/services/match/src/app/api/routes/pairs.py @@ -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), diff --git a/services/payments/Dockerfile b/services/payments/Dockerfile index 57c86d2..831bed9 100644 --- a/services/payments/Dockerfile +++ b/services/payments/Dockerfile @@ -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 ./ diff --git a/services/payments/docker-entrypoint.sh.bak.1754801031 b/services/payments/docker-entrypoint.sh.bak.1754801031 new file mode 100755 index 0000000..2828898 --- /dev/null +++ b/services/payments/docker-entrypoint.sh.bak.1754801031 @@ -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 diff --git a/services/profiles/Dockerfile b/services/profiles/Dockerfile index 57c86d2..0188e43 100644 --- a/services/profiles/Dockerfile +++ b/services/profiles/Dockerfile @@ -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 ./ diff --git a/services/profiles/alembic/versions/add_profile_photo_and_likes_1754798354.py b/services/profiles/alembic/versions/add_profile_photo_and_likes_1754798354.py new file mode 100644 index 0000000..961dd7b --- /dev/null +++ b/services/profiles/alembic/versions/add_profile_photo_and_likes_1754798354.py @@ -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') diff --git a/services/profiles/docker-entrypoint.sh.bak.1754801031 b/services/profiles/docker-entrypoint.sh.bak.1754801031 new file mode 100755 index 0000000..ae2ee5e --- /dev/null +++ b/services/profiles/docker-entrypoint.sh.bak.1754801031 @@ -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 diff --git a/services/profiles/requirements.txt b/services/profiles/requirements.txt index 1b92356..21775d3 100644 --- a/services/profiles/requirements.txt +++ b/services/profiles/requirements.txt @@ -9,3 +9,4 @@ python-dotenv httpx>=0.27 pytest PyJWT>=2.8 +python-multipart==0.0.9 diff --git a/services/profiles/src/app/api/routes/profiles.py b/services/profiles/src/app/api/routes/profiles.py index 437b933..019d3e3 100644 --- a/services/profiles/src/app/api/routes/profiles.py +++ b/services/profiles/src/app/api/routes/profiles.py @@ -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/ + 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) diff --git a/services/profiles/src/app/api/routes/profiles.py.bak.1754798354 b/services/profiles/src/app/api/routes/profiles.py.bak.1754798354 new file mode 100644 index 0000000..437b933 --- /dev/null +++ b/services/profiles/src/app/api/routes/profiles.py.bak.1754798354 @@ -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) diff --git a/services/profiles/src/app/api/routes/profiles.py.bak.1754798570 b/services/profiles/src/app/api/routes/profiles.py.bak.1754798570 new file mode 100644 index 0000000..e3c7d05 --- /dev/null +++ b/services/profiles/src/app/api/routes/profiles.py.bak.1754798570 @@ -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/ + 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) diff --git a/services/profiles/src/app/api/routes/profiles.py.bak.1754798889 b/services/profiles/src/app/api/routes/profiles.py.bak.1754798889 new file mode 100644 index 0000000..e1813ab --- /dev/null +++ b/services/profiles/src/app/api/routes/profiles.py.bak.1754798889 @@ -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/ + 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) diff --git a/services/profiles/src/app/core/security.py b/services/profiles/src/app/core/security.py index e179799..98b4c84 100644 --- a/services/profiles/src/app/core/security.py +++ b/services/profiles/src/app/core/security.py @@ -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 --- diff --git a/services/profiles/src/app/core/security.py.bak.1754799021 b/services/profiles/src/app/core/security.py.bak.1754799021 new file mode 100644 index 0000000..e179799 --- /dev/null +++ b/services/profiles/src/app/core/security.py.bak.1754799021 @@ -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) diff --git a/services/profiles/src/app/core/security.py.bak.1754799216 b/services/profiles/src/app/core/security.py.bak.1754799216 new file mode 100644 index 0000000..a70d3ea --- /dev/null +++ b/services/profiles/src/app/core/security.py.bak.1754799216 @@ -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 --- diff --git a/services/profiles/src/app/main.py b/services/profiles/src/app/main.py index a903e4f..33111a4 100644 --- a/services/profiles/src/app/main.py +++ b/services/profiles/src/app/main.py @@ -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) diff --git a/services/profiles/src/app/models/like.py b/services/profiles/src/app/models/like.py new file mode 100644 index 0000000..a1687c5 --- /dev/null +++ b/services/profiles/src/app/models/like.py @@ -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'), + ) diff --git a/services/profiles/src/app/repositories/likes_repository.py b/services/profiles/src/app/repositories/likes_repository.py new file mode 100644 index 0000000..ba1089e --- /dev/null +++ b/services/profiles/src/app/repositories/likes_repository.py @@ -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] diff --git a/services/profiles/src/app/repositories/profile_repository.py b/services/profiles/src/app/repositories/profile_repository.py index 3ad39eb..b4fde0b 100644 --- a/services/profiles/src/app/repositories/profile_repository.py +++ b/services/profiles/src/app/repositories/profile_repository.py @@ -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 diff --git a/services/profiles/src/app/repositories/profile_repository.py.bak.1754800435 b/services/profiles/src/app/repositories/profile_repository.py.bak.1754800435 new file mode 100644 index 0000000..3ad39eb --- /dev/null +++ b/services/profiles/src/app/repositories/profile_repository.py.bak.1754800435 @@ -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 diff --git a/services/profiles/src/app/repositories/profile_search_repository.py b/services/profiles/src/app/repositories/profile_search_repository.py new file mode 100644 index 0000000..7aaa4c7 --- /dev/null +++ b/services/profiles/src/app/repositories/profile_search_repository.py @@ -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() diff --git a/services/profiles/src/app/schemas/profile.py b/services/profiles/src/app/schemas/profile.py index 77c7606..573e97a 100644 --- a/services/profiles/src/app/schemas/profile.py +++ b/services/profiles/src/app/schemas/profile.py @@ -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: - model_config = ConfigDict(from_attributes=True) - else: - class Config: - orm_mode = True + # Важно: для возврата ORM-объектов + model_config = ConfigDict(from_attributes=True) + + +# Для лайков +class LikesList(BaseModel): + items: List[UUID] diff --git a/services/profiles/src/app/schemas/profile.py.bak.1754798354 b/services/profiles/src/app/schemas/profile.py.bak.1754798354 new file mode 100644 index 0000000..3bfb1ac --- /dev/null +++ b/services/profiles/src/app/schemas/profile.py.bak.1754798354 @@ -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 diff --git a/services/profiles/src/app/schemas/profile.py.bak.1754800672 b/services/profiles/src/app/schemas/profile.py.bak.1754800672 new file mode 100644 index 0000000..2c64d04 --- /dev/null +++ b/services/profiles/src/app/schemas/profile.py.bak.1754800672 @@ -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] diff --git a/services/profiles/src/app/services/likes_service.py b/services/profiles/src/app/services/likes_service.py new file mode 100644 index 0000000..5c63dde --- /dev/null +++ b/services/profiles/src/app/services/likes_service.py @@ -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) diff --git a/services/profiles/src/app/services/profile_search_service.py b/services/profiles/src/app/services/profile_search_service.py new file mode 100644 index 0000000..27644cc --- /dev/null +++ b/services/profiles/src/app/services/profile_search_service.py @@ -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) diff --git a/services/profiles/src/app/services/profile_service.py b/services/profiles/src/app/services/profile_service.py index 2970a7b..c5a1b58 100644 --- a/services/profiles/src/app/services/profile_service.py +++ b/services/profiles/src/app/services/profile_service.py @@ -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) diff --git a/services/profiles/src/app/services/profile_service.py.bak.1754798354 b/services/profiles/src/app/services/profile_service.py.bak.1754798354 new file mode 100644 index 0000000..2970a7b --- /dev/null +++ b/services/profiles/src/app/services/profile_service.py.bak.1754798354 @@ -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)