diff --git a/.env.development b/.env.development index 38a3afc..0f88f9d 100644 --- a/.env.development +++ b/.env.development @@ -19,8 +19,16 @@ API_KEY_PREFIX=ce_dev_ KUBERNETES_NAMESPACE=container-engine-dev # Domain Configuration -DOMAIN_SUFFIX=vinhomes.co.uk +DOMAIN_SUFFIX=your_domain_suffix + +# Email Configuration - Mailtrap +MAILTRAP_SMTP_HOST=your_host +MAILTRAP_SMTP_PORT=587 +MAILTRAP_USERNAME=your_mailtrap_username +MAILTRAP_PASSWORD=your_mailtrap_password +EMAIL_FROM=your_email +EMAIL_FROM_NAME=your_app_name # Logging RUST_LOG=container_engine=debug,tower_http=debug -KUBECONFIG_PATH=./k8sConfig.yaml +KUBECONFIG_PATH=./k8sConfig.yaml \ No newline at end of file diff --git a/.env.example b/.env.example index 00ff960..1e926c6 100644 --- a/.env.example +++ b/.env.example @@ -18,7 +18,17 @@ API_KEY_PREFIX=ce_api_ KUBERNETES_NAMESPACE=container-engine # Domain Configuration -DOMAIN_SUFFIX=container-engine.app +DOMAIN_SUFFIX=your_domain_suffix + +# Email Configuration - Mailtrap Live SMTP +MAILTRAP_SMTP_HOST=your_host +MAILTRAP_SMTP_PORT=587 +MAILTRAP_USERNAME=your_mailtrap_username +MAILTRAP_PASSWORD=your_mailtrap_password +EMAIL_FROM=noreply@yourdomain.comapp +EMAIL_FROM_NAME=your_app + + # Logging RUST_LOG=container_engine=debug,tower_http=debug diff --git a/.sqlx/query-19945d2e23d4e54bcaafd212df71faecd762789418a15e68cedc88ef666fec8d.json b/.sqlx/query-19945d2e23d4e54bcaafd212df71faecd762789418a15e68cedc88ef666fec8d.json new file mode 100644 index 0000000..639a378 --- /dev/null +++ b/.sqlx/query-19945d2e23d4e54bcaafd212df71faecd762789418a15e68cedc88ef666fec8d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE password_reset_tokens SET used_at = NOW(), updated_at = NOW() WHERE token = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "19945d2e23d4e54bcaafd212df71faecd762789418a15e68cedc88ef666fec8d" +} diff --git a/.sqlx/query-2cbdf5c505a0a7d65eb01c482a4ab9378701e7d675d45e4fcb7404aba853d589.json b/.sqlx/query-2cbdf5c505a0a7d65eb01c482a4ab9378701e7d675d45e4fcb7404aba853d589.json new file mode 100644 index 0000000..e1e9675 --- /dev/null +++ b/.sqlx/query-2cbdf5c505a0a7d65eb01c482a4ab9378701e7d675d45e4fcb7404aba853d589.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM password_reset_tokens WHERE user_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "2cbdf5c505a0a7d65eb01c482a4ab9378701e7d675d45e4fcb7404aba853d589" +} diff --git a/.sqlx/query-32aa79a0a9654fa2e3acbbd0f6ed64c6ea32b916e841115921886c0049ee8958.json b/.sqlx/query-32aa79a0a9654fa2e3acbbd0f6ed64c6ea32b916e841115921886c0049ee8958.json new file mode 100644 index 0000000..e54407e --- /dev/null +++ b/.sqlx/query-32aa79a0a9654fa2e3acbbd0f6ed64c6ea32b916e841115921886c0049ee8958.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM user_webhooks WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "secret", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "is_active", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "events", + "type_info": "TextArray" + }, + { + "ordinal": 7, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "32aa79a0a9654fa2e3acbbd0f6ed64c6ea32b916e841115921886c0049ee8958" +} diff --git a/.sqlx/query-38bba728896ac61542b8279a6b5a2f6740dfd3ac6b9264c5554429824338ccea.json b/.sqlx/query-38bba728896ac61542b8279a6b5a2f6740dfd3ac6b9264c5554429824338ccea.json new file mode 100644 index 0000000..ff5580e --- /dev/null +++ b/.sqlx/query-38bba728896ac61542b8279a6b5a2f6740dfd3ac6b9264c5554429824338ccea.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM user_webhooks WHERE id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "38bba728896ac61542b8279a6b5a2f6740dfd3ac6b9264c5554429824338ccea" +} diff --git a/.sqlx/query-3f4d708c6810b85421669ada5f45b3a9730d85635ac5f63baa9e197895fd9005.json b/.sqlx/query-3f4d708c6810b85421669ada5f45b3a9730d85635ac5f63baa9e197895fd9005.json new file mode 100644 index 0000000..fe3d3a9 --- /dev/null +++ b/.sqlx/query-3f4d708c6810b85421669ada5f45b3a9730d85635ac5f63baa9e197895fd9005.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM user_webhooks WHERE user_id = $1 ORDER BY created_at DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "secret", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "is_active", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "events", + "type_info": "TextArray" + }, + { + "ordinal": 7, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "3f4d708c6810b85421669ada5f45b3a9730d85635ac5f63baa9e197895fd9005" +} diff --git a/.sqlx/query-5889f23a18bf32671c979a8a18261ab4ca9c7170290effe5f2461cb492c03fd2.json b/.sqlx/query-5889f23a18bf32671c979a8a18261ab4ca9c7170290effe5f2461cb492c03fd2.json new file mode 100644 index 0000000..6322d69 --- /dev/null +++ b/.sqlx/query-5889f23a18bf32671c979a8a18261ab4ca9c7170290effe5f2461cb492c03fd2.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE user_webhooks \n SET \n name = COALESCE($1, name),\n url = COALESCE($2, url),\n secret = COALESCE($3, secret),\n is_active = COALESCE($4, is_active),\n events = COALESCE($5, events),\n updated_at = NOW()\n WHERE id = $6\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Bool", + "TextArray", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "5889f23a18bf32671c979a8a18261ab4ca9c7170290effe5f2461cb492c03fd2" +} diff --git a/.sqlx/query-6291008bd24d8a6f907e71c0040f604f96bdee291cda04d37fddff52d92e92a0.json b/.sqlx/query-6291008bd24d8a6f907e71c0040f604f96bdee291cda04d37fddff52d92e92a0.json new file mode 100644 index 0000000..2a5f0b9 --- /dev/null +++ b/.sqlx/query-6291008bd24d8a6f907e71c0040f604f96bdee291cda04d37fddff52d92e92a0.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM user_webhooks WHERE id = $1 AND user_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "6291008bd24d8a6f907e71c0040f604f96bdee291cda04d37fddff52d92e92a0" +} diff --git a/.sqlx/query-6319c10d4f5c165e2e3c5eeec8230922e2d15325d2cee9c73bd90c0be4cd3046.json b/.sqlx/query-6319c10d4f5c165e2e3c5eeec8230922e2d15325d2cee9c73bd90c0be4cd3046.json new file mode 100644 index 0000000..de932b5 --- /dev/null +++ b/.sqlx/query-6319c10d4f5c165e2e3c5eeec8230922e2d15325d2cee9c73bd90c0be4cd3046.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM user_webhooks WHERE id = $1 AND user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "secret", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "is_active", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "events", + "type_info": "TextArray" + }, + { + "ordinal": 7, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "6319c10d4f5c165e2e3c5eeec8230922e2d15325d2cee9c73bd90c0be4cd3046" +} diff --git a/.sqlx/query-ac2818b04f46f739d803b90a12e93dd8e587fc9e51b0fb414f5773f30fbcd6c8.json b/.sqlx/query-ac2818b04f46f739d803b90a12e93dd8e587fc9e51b0fb414f5773f30fbcd6c8.json new file mode 100644 index 0000000..7ca6138 --- /dev/null +++ b/.sqlx/query-ac2818b04f46f739d803b90a12e93dd8e587fc9e51b0fb414f5773f30fbcd6c8.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_webhooks (\n id, user_id, name, url, secret, is_active, events, created_at, updated_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "TextArray", + "Timestamptz", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "ac2818b04f46f739d803b90a12e93dd8e587fc9e51b0fb414f5773f30fbcd6c8" +} diff --git a/.sqlx/query-bafd62b66947dc636dd2990d969223aa3f5d25a3128140a9310f9d91cc7cf513.json b/.sqlx/query-bafd62b66947dc636dd2990d969223aa3f5d25a3128140a9310f9d91cc7cf513.json new file mode 100644 index 0000000..c369497 --- /dev/null +++ b/.sqlx/query-bafd62b66947dc636dd2990d969223aa3f5d25a3128140a9310f9d91cc7cf513.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT user_id, expires_at, used_at\n FROM password_reset_tokens \n WHERE token = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "used_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "bafd62b66947dc636dd2990d969223aa3f5d25a3128140a9310f9d91cc7cf513" +} diff --git a/.sqlx/query-d1db3347028467d3a35f5c85f7ea22f8f832bfa16f2065135f8f53bcd8c0fa1f.json b/.sqlx/query-d1db3347028467d3a35f5c85f7ea22f8f832bfa16f2065135f8f53bcd8c0fa1f.json new file mode 100644 index 0000000..33ddd06 --- /dev/null +++ b/.sqlx/query-d1db3347028467d3a35f5c85f7ea22f8f832bfa16f2065135f8f53bcd8c0fa1f.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO password_reset_tokens (user_id, token, expires_at, created_at, updated_at)\n VALUES ($1, $2, $3, NOW(), NOW())\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "d1db3347028467d3a35f5c85f7ea22f8f832bfa16f2065135f8f53bcd8c0fa1f" +} diff --git a/.sqlx/query-e6e92e69ca96e5085874a0e8142d66a3d28cd32e5bb7e47e46392f935c3e6339.json b/.sqlx/query-e6e92e69ca96e5085874a0e8142d66a3d28cd32e5bb7e47e46392f935c3e6339.json new file mode 100644 index 0000000..6a742e2 --- /dev/null +++ b/.sqlx/query-e6e92e69ca96e5085874a0e8142d66a3d28cd32e5bb7e47e46392f935c3e6339.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT url FROM user_webhooks WHERE id = $1 AND user_id = $2 AND is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "url", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e6e92e69ca96e5085874a0e8142d66a3d28cd32e5bb7e47e46392f935c3e6339" +} diff --git a/.sqlx/query-ea944c26183e9b1181ccb75b26abc618f85d8cb78920e3374aec08ee68bc8ef9.json b/.sqlx/query-ea944c26183e9b1181ccb75b26abc618f85d8cb78920e3374aec08ee68bc8ef9.json new file mode 100644 index 0000000..6cdefbd --- /dev/null +++ b/.sqlx/query-ea944c26183e9b1181ccb75b26abc618f85d8cb78920e3374aec08ee68bc8ef9.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, url, secret \n FROM user_webhooks \n WHERE user_id = $1 AND is_active = true \n AND ($2 = ANY(events) OR 'all' = ANY(events))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "secret", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "ea944c26183e9b1181ccb75b26abc618f85d8cb78920e3374aec08ee68bc8ef9" +} diff --git a/.sqlx/query-ee4598c953a21125db0c7506c9f40e53af91be8afe5d6a76681deb856486545c.json b/.sqlx/query-ee4598c953a21125db0c7506c9f40e53af91be8afe5d6a76681deb856486545c.json new file mode 100644 index 0000000..f5969cf --- /dev/null +++ b/.sqlx/query-ee4598c953a21125db0c7506c9f40e53af91be8afe5d6a76681deb856486545c.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM user_webhooks WHERE user_id = $1 AND name = $2 AND id != $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ee4598c953a21125db0c7506c9f40e53af91be8afe5d6a76681deb856486545c" +} diff --git a/.sqlx/query-fea9a78339b5e89b18a59301e395a51ae8ff746a092b325fa4f703c1906165cf.json b/.sqlx/query-fea9a78339b5e89b18a59301e395a51ae8ff746a092b325fa4f703c1906165cf.json new file mode 100644 index 0000000..200c0bf --- /dev/null +++ b/.sqlx/query-fea9a78339b5e89b18a59301e395a51ae8ff746a092b325fa4f703c1906165cf.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM user_webhooks WHERE user_id = $1 AND name = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fea9a78339b5e89b18a59301e395a51ae8ff746a092b325fa4f703c1906165cf" +} diff --git a/APIs.md b/APIs.md index ec28db1..bb1842b 100644 --- a/APIs.md +++ b/APIs.md @@ -5,9 +5,13 @@ This document provides comprehensive documentation for all API endpoints availab ## Base URL ``` -https://api.container-engine.app +http://localhost:3000 ``` +For production deployment: Replace with your actual domain. + +**Domain Suffix:** `vinhomes.co.uk` (configured in environment) + ## Authentication All API endpoints (except registration and login) require authentication via API key in the Authorization header: @@ -37,18 +41,21 @@ Content-Type: application/json "username": "string", "email": "string", "password": "string", - "confirmPassword": "string" + "confirm_password": "string" } ``` **Response (201 Created):** ```json { - "id": "usr-a1b2c3d4e5", - "username": "string", - "email": "string", - "createdAt": "2025-01-01T00:00:00Z", - "status": "active" + "access_token": "string", + "refresh_token": "string", + "expires_at": "2025-01-01T01:00:00Z", + "user": { + "id": "uuid", + "username": "string", + "email": "string" + } } ``` @@ -80,11 +87,11 @@ Content-Type: application/json **Response (200 OK):** ```json { - "accessToken": "string", - "refreshToken": "string", - "expiresAt": "2025-01-01T01:00:00Z", + "access_token": "string", + "refresh_token": "string", + "expires_at": "2025-01-01T01:00:00Z", "user": { - "id": "usr-a1b2c3d4e5", + "id": "uuid", "username": "string", "email": "string" } @@ -111,15 +118,15 @@ Content-Type: application/json **Request Body:** ```json { - "refreshToken": "string" + "refresh_token": "string" } ``` **Response (200 OK):** ```json { - "accessToken": "string", - "expiresAt": "2025-01-01T01:00:00Z" + "access_token": "string", + "expires_at": "2025-01-01T01:00:00Z" } ``` @@ -145,6 +152,61 @@ Authorization: Bearer --- +## Forgot Password + +Request a password reset link. + +**Endpoint:** `POST /v1/auth/forgot-password` + +**Headers:** +```http +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "email": "string" +} +``` + +**Response (200 OK):** +```json +{ + "message": "If an account with that email exists, a password reset link has been sent." +} +``` + +--- + +## Reset Password + +Reset password using a token. + +**Endpoint:** `POST /v1/auth/reset-password` + +**Headers:** +```http +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "token": "string", + "new_password": "string" +} +``` + +**Response (200 OK):** +```json +{ + "message": "Password reset successfully" +} +``` + +--- + ## API Key Management ### Create API Key @@ -163,21 +225,17 @@ Content-Type: application/json ```json { "name": "string", - "description": "string", - "expiresAt": "2025-12-31T23:59:59Z" // optional + "description": "string" } ``` **Response (201 Created):** ```json { - "id": "key-a1b2c3d4e5", - "name": "string", - "description": "string", - "apiKey": "ce_api_1234567890abcdef...", - "createdAt": "2025-01-01T00:00:00Z", - "expiresAt": "2025-12-31T23:59:59Z", - "lastUsed": null + "id": "uuid", + "name": "string", + "key": "ce_dev_1234567890abcdef...", + "created_at": "2025-01-01T00:00:00Z" } ``` @@ -201,21 +259,22 @@ Authorization: Bearer **Response (200 OK):** ```json { - "apiKeys": [ + "api_keys": [ { - "id": "key-a1b2c3d4e5", + "id": "uuid", "name": "string", "description": "string", - "createdAt": "2025-01-01T00:00:00Z", - "expiresAt": "2025-12-31T23:59:59Z", - "lastUsed": "2025-01-01T12:00:00Z" + "key_prefix": "ce_dev_", + "created_at": "2025-01-01T00:00:00Z", + "expires_at": "2025-12-31T23:59:59Z", + "last_used": "2025-01-01T12:00:00Z" } ], "pagination": { "page": 1, "limit": 10, "total": 1, - "totalPages": 1 + "total_pages": 1 } } ``` @@ -258,13 +317,12 @@ Authorization: Bearer **Response (200 OK):** ```json { - "id": "usr-a1b2c3d4e5", + "id": "uuid", "username": "string", "email": "string", - "createdAt": "2025-01-01T00:00:00Z", - "lastLogin": "2025-01-01T12:00:00Z", - "deploymentCount": 5, - "apiKeyCount": 2 + "created_at": "2025-01-01T00:00:00Z", + "last_login": "2025-01-01T12:00:00Z", + "is_active": true } ``` @@ -293,10 +351,10 @@ Content-Type: application/json **Response (200 OK):** ```json { - "id": "usr-a1b2c3d4e5", + "id": "uuid", "username": "string", "email": "string", - "updatedAt": "2025-01-01T12:00:00Z" + "updated_at": "2025-01-01T12:00:00Z" } ``` @@ -317,9 +375,9 @@ Content-Type: application/json **Request Body:** ```json { - "currentPassword": "string", - "newPassword": "string", - "confirmNewPassword": "string" + "current_password": "string", + "new_password": "string", + "confirm_new_password": "string" } ``` @@ -349,25 +407,25 @@ Content-Type: application/json **Request Body:** ```json { - "appName": "string", + "app_name": "string", "image": "string", "port": 80, - "envVars": { + "env_vars": { "ENV_VAR_NAME": "value" }, - "replicas": 1, // optional, default: 1 - "resources": { // optional + "replicas": 1, + "resources": { "cpu": "100m", "memory": "128Mi" }, - "healthCheck": { // optional + "health_check": { "path": "/health", - "initialDelaySeconds": 5, - "periodSeconds": 10, - "timeoutSeconds": 5, - "failureThreshold": 3 + "initial_delay_seconds": 30, + "period_seconds": 10, + "timeout_seconds": 5, + "failure_threshold": 3 }, - "registryAuth": { // optional, for private registries + "registry_auth": { "username": "string", "password": "string" } @@ -377,12 +435,12 @@ Content-Type: application/json **Response (201 Created):** ```json { - "id": "dpl-a1b2c3d4e5", - "appName": "string", + "id": "uuid", + "app_name": "string", "image": "string", "status": "pending", - "url": "https://app-name.container-engine.app", - "createdAt": "2025-01-01T00:00:00Z", + "url": null, + "created_at": "2025-01-01T00:00:00Z", "message": "Deployment is being processed" } ``` @@ -415,21 +473,21 @@ Authorization: Bearer { "deployments": [ { - "id": "dpl-a1b2c3d4e5", - "appName": "string", + "id": "uuid", + "app_name": "string", "image": "string", "status": "running", - "url": "https://app-name.container-engine.app", + "url": "https://app-name.vinhomes.co.uk", "replicas": 1, - "createdAt": "2025-01-01T00:00:00Z", - "updatedAt": "2025-01-01T00:05:00Z" + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:05:00Z" } ], "pagination": { "page": 1, "limit": 10, "total": 1, - "totalPages": 1 + "total_pages": 1 } } ``` @@ -450,30 +508,32 @@ Authorization: Bearer **Response (200 OK):** ```json { - "id": "dpl-a1b2c3d4e5", - "appName": "string", + "id": "uuid", + "user_id": "uuid", + "app_name": "string", "image": "string", "status": "running", - "url": "https://app-name.container-engine.app", + "url": "https://app-name.vinhomes.co.uk", "port": 80, - "replicas": 1, - "envVars": { + "env_vars": { "ENV_VAR_NAME": "value" }, + "replicas": 1, "resources": { "cpu": "100m", "memory": "128Mi" }, - "healthCheck": { + "health_check": { "path": "/health", - "initialDelaySeconds": 5, - "periodSeconds": 10, - "timeoutSeconds": 5, - "failureThreshold": 3 + "initial_delay_seconds": 30, + "period_seconds": 10, + "timeout_seconds": 5, + "failure_threshold": 3 }, - "createdAt": "2025-01-01T00:00:00Z", - "updatedAt": "2025-01-01T00:05:00Z", - "deployedAt": "2025-01-01T00:05:00Z" + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:05:00Z", + "deployed_at": "2025-01-01T00:05:00Z", + "error_message": null } ``` @@ -494,12 +554,12 @@ Content-Type: application/json **Request Body:** ```json { - "image": "string", // optional - "envVars": { // optional + "image": "string", + "env_vars": { "ENV_VAR_NAME": "new_value" }, - "replicas": 2, // optional - "resources": { // optional + "replicas": 2, + "resources": { "cpu": "200m", "memory": "256Mi" } @@ -509,10 +569,10 @@ Content-Type: application/json **Response (200 OK):** ```json { - "id": "dpl-a1b2c3d4e5", + "id": "uuid", "status": "updating", "message": "Deployment update in progress", - "updatedAt": "2025-01-01T12:00:00Z" + "updated_at": "2025-01-01T12:00:00Z" } ``` @@ -540,10 +600,10 @@ Content-Type: application/json **Response (200 OK):** ```json { - "id": "dpl-a1b2c3d4e5", + "id": "uuid", "replicas": 5, - "status": "scaling", - "message": "Deployment scaling in progress" + "status": "running", + "message": "Deployment scaled successfully" } ``` @@ -563,9 +623,9 @@ Authorization: Bearer **Response (200 OK):** ```json { - "id": "dpl-a1b2c3d4e5", - "status": "stopping", - "message": "Deployment is being stopped" + "id": "uuid", + "status": "stopped", + "message": "Deployment stopped successfully" } ``` @@ -585,9 +645,10 @@ Authorization: Bearer **Response (200 OK):** ```json { - "id": "dpl-a1b2c3d4e5", - "status": "starting", - "message": "Deployment is being started" + "id": "uuid", + "status": "running", + "replicas": 1, + "message": "Deployment started successfully" } ``` @@ -607,7 +668,10 @@ Authorization: Bearer **Response (200 OK):** ```json { - "message": "Deployment deleted successfully" + "message": "Deployment deleted successfully", + "deployment_id": "uuid", + "app_name": "string", + "namespace_deleted": true } ``` @@ -634,20 +698,12 @@ Content-Type: application/json } ``` -**Response (201 Created):** +**Response (500 Internal Server Error):** ```json { - "id": "dom-a1b2c3d4e5", - "domain": "myapp.example.com", - "status": "pending", - "createdAt": "2025-01-01T00:00:00Z", - "dnsRecords": [ - { - "type": "CNAME", - "name": "myapp.example.com", - "value": "app-name.container-engine.app" - } - ] + "error": { + "message": "Domain management not yet implemented" + } } ``` @@ -667,15 +723,7 @@ Authorization: Bearer **Response (200 OK):** ```json { - "domains": [ - { - "id": "dom-a1b2c3d4e5", - "domain": "myapp.example.com", - "status": "active", - "createdAt": "2025-01-01T00:00:00Z", - "verifiedAt": "2025-01-01T00:15:00Z" - } - ] + "domains": [] } ``` @@ -692,10 +740,12 @@ Remove a custom domain from a deployment. Authorization: Bearer ``` -**Response (200 OK):** +**Response (500 Internal Server Error):** ```json { - "message": "Custom domain removed successfully" + "error": { + "message": "Domain management not yet implemented" + } } ``` @@ -717,8 +767,6 @@ Authorization: Bearer **Query Parameters:** - `tail` (optional): Number of lines to return from the end (default: 100) - `follow` (optional): Stream logs in real-time (default: false) -- `since` (optional): Return logs since timestamp (ISO 8601) -- `until` (optional): Return logs until timestamp (ISO 8601) **Response (200 OK):** ```json @@ -726,7 +774,7 @@ Authorization: Bearer "logs": [ { "timestamp": "2025-01-01T12:00:00Z", - "level": "info", + "level": "info", "message": "Application started successfully", "source": "app" } @@ -736,7 +784,7 @@ Authorization: Bearer **WebSocket Endpoint for Real-time Logs:** ``` -wss://api.container-engine.app/v1/deployments/{deploymentId}/logs/stream +ws://localhost:3000/v1/deployments/{deploymentId}/logs/stream ``` --- @@ -752,37 +800,15 @@ Get performance metrics for a deployment. Authorization: Bearer ``` -**Query Parameters:** -- `from` (optional): Start time for metrics (ISO 8601) -- `to` (optional): End time for metrics (ISO 8601) -- `resolution` (optional): Metric resolution (1m, 5m, 1h, 1d) - **Response (200 OK):** ```json { - "metrics": { - "cpu": [ - { - "timestamp": "2025-01-01T12:00:00Z", - "value": 0.25 - } - ], - "memory": [ - { - "timestamp": "2025-01-01T12:00:00Z", - "value": 134217728 - } - ], - "requests": [ - { - "timestamp": "2025-01-01T12:00:00Z", - "value": 100 - } - ] - } + "metrics": {} } ``` +Note: Metrics implementation is currently a stub. + --- ### Get Deployment Status @@ -806,14 +832,89 @@ Authorization: Bearer "ready": 2, "available": 2 }, - "lastHealthCheck": "2025-01-01T12:00:00Z", - "uptime": "2h 30m 45s", - "restartCount": 0 + "last_health_check": "2025-01-01T12:00:00Z", + "uptime": "0s", + "restart_count": 0 +} +``` + +--- + +## Health Check + +Check the overall health of the Container Engine API. + +**Endpoint:** `GET /health` + +**Response (200 OK):** +```json +{ + "status": "healthy", + "service": "container-engine", + "version": "0.1.0" +} +``` + +--- + +## WebSocket Notifications + +Real-time notifications are available via WebSocket connection: + +**Connection URL:** +``` +ws://localhost:3000/v1/ws/notifications +``` + +**Health Check URL:** +``` +ws://localhost:3000/v1/ws/health +``` + +**Authentication:** +Include Authorization header with Bearer token when connecting. + +**Event Format:** +```json +{ + "type": "deployment_status_changed", + "deployment_id": "uuid", + "timestamp": "2025-01-01T12:00:00Z", + "data": { + "status": "running", + "url": "https://app-name.vinhomes.co.uk" + } } ``` --- +## Testing Endpoints + +### Send Test Notification + +Trigger a test notification (for development). + +**Endpoint:** `GET /v1/notifications/test` + +**Headers:** +```http +Authorization: Bearer +``` + +### Get Notification Stats + +Get notification system statistics. + +**Endpoint:** `GET /v1/notifications/stats` + +**Headers:** +```http +Authorization: Bearer +``` + +--- + ## Error Handling All API endpoints return consistent error responses: @@ -822,79 +923,64 @@ All API endpoints return consistent error responses: ```json { "error": { - "code": "string", "message": "string", - "details": {} // optional additional error details + "details": "string" } } ``` **Common HTTP Status Codes:** - `400 Bad Request`: Invalid request parameters -- `401 Unauthorized`: Missing or invalid authentication +- `401 Unauthorized`: Missing or invalid authentication - `403 Forbidden`: Insufficient permissions - `404 Not Found`: Resource not found -- `409 Conflict`: Resource conflict (e.g., duplicate name) +- `409 Conflict`: Resource conflict (e.g., duplicate app name) - `422 Unprocessable Entity`: Invalid data format -- `429 Too Many Requests`: Rate limit exceeded - `500 Internal Server Error`: Server error --- -## Rate Limiting +## Environment Configuration -API requests are rate-limited per API key: +The API uses the following environment variables: -- **Authentication endpoints**: 10 requests per minute -- **Deployment operations**: 30 requests per minute -- **Read operations**: 100 requests per minute -- **Logs and metrics**: 60 requests per minute +**Required:** +- `DATABASE_URL`: PostgreSQL connection string +- `REDIS_URL`: Redis connection string +- `JWT_SECRET`: Secret key for JWT tokens -Rate limit headers are included in all responses: -```http -X-RateLimit-Limit: 100 -X-RateLimit-Remaining: 99 -X-RateLimit-Reset: 1641024000 -``` +**Optional:** +- `PORT`: Server port (default: 3000) +- `DOMAIN_SUFFIX`: Domain suffix for deployments (default: vinhomes.co.uk) +- `WEBHOOK_URL`: Webhook URL for deployment events +- `KUBECONFIG_PATH`: Path to Kubernetes config file +- `KUBERNETES_NAMESPACE`: Default Kubernetes namespace ---- +**Email Configuration (Optional):** +- `AWS_SES_SMTP_HOST`: SMTP host for email service +- `AWS_SES_SMTP_PORT`: SMTP port (default: 587) +- `AWS_SES_USERNAME`: SMTP username +- `AWS_SES_PASSWORD`: SMTP password +- `EMAIL_FROM`: From email address +- `EMAIL_FROM_NAME`: From email name -## WebSocket Events +--- -Real-time events are available via WebSocket connection: +## Webhook Integration -**Connection URL:** -``` -wss://api.container-engine.app/v1/events -``` - -**Authentication:** -Include API key in connection query parameter: -``` -wss://api.container-engine.app/v1/events?token= -``` +When enabled, the API sends webhook notifications for deployment events to the configured `WEBHOOK_URL`. -**Event Format:** +**Webhook Payload:** ```json { - "type": "deployment.status.changed", - "deploymentId": "dpl-a1b2c3d4e5", + "deployment_id": "uuid", + "status": "completed|failed|started", + "type": "deployment_completed|deployment_failed|deployment_started", "timestamp": "2025-01-01T12:00:00Z", - "data": { - "status": "running", - "previousStatus": "pending" - } + "app_name": "string", + "user_id": "uuid", + "url": "https://app-name.vinhomes.co.uk" } ``` -**Available Event Types:** -- `deployment.created` -- `deployment.status.changed` -- `deployment.updated` -- `deployment.deleted` -- `domain.verified` -- `domain.failed` - ---- - -This API documentation covers all the essential endpoints for a complete container engine platform with user management, authentication, deployment management, and monitoring capabilities. \ No newline at end of file +--- \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d473b2c..913b4eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "MIT" repository = "https://github.com/ngocbd/Open-Container-Engine" [dependencies] +lettre = "0.11" serde_yaml = "0.9" bytes = "1.5" futures-util = "0.3" diff --git a/WEBHOOK_SYSTEM.md b/WEBHOOK_SYSTEM.md new file mode 100644 index 0000000..e69de29 diff --git a/apps/container-engine-frontend/src/App.tsx b/apps/container-engine-frontend/src/App.tsx index 1dda07d..5fbd01b 100644 --- a/apps/container-engine-frontend/src/App.tsx +++ b/apps/container-engine-frontend/src/App.tsx @@ -14,6 +14,7 @@ import FeaturesPage from './pages/FeaturesPage'; import DocumentationPage from './pages/DocumentationPage'; import PrivacyPolicyPage from './pages/PrivacyPolicyPage'; import TermsOfServicePage from './pages/TermsOfServicePage'; +import WebhooksPage from './pages/WebhooksPage'; import ProtectedRoute from './components/ProtectedRoute'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -40,6 +41,7 @@ function App() { } /> } /> } /> + } /> } /> {/* Default route */} diff --git a/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx b/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx index 22c9023..68c678b 100644 --- a/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx +++ b/apps/container-engine-frontend/src/components/DeploymentDetail/LogsPage.tsx @@ -1,19 +1,22 @@ // LogsPage.jsx import { useState, useEffect, useRef } from 'react'; -import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; +import { ClipboardDocumentListIcon, CubeIcon } from "@heroicons/react/24/outline"; import { useParams } from 'react-router-dom'; import api from '../../api/api'; export default function LogsPage() { const { deploymentId } = useParams(); const [logs, setLogs] = useState([]); + const [pods, setPods] = useState([]); + const [selectedPod, setSelectedPod] = useState('all'); // 'all' or specific pod name const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isLoadingHistory, setIsLoadingHistory] = useState(false); + const [isLoadingPods, setIsLoadingPods] = useState(false); const [error, setError] = useState(null); - const wsRef: any = useRef(null); + const wsRef = useRef(null); const logsEndRef: any = useRef(null); - const reconnectTimeoutRef: any = useRef(null); + const reconnectTimeoutRef = useRef(null); const reconnectDelay = useRef(1000); // Auto-scroll to bottom when new logs arrive @@ -36,6 +39,29 @@ export default function LogsPage() { return null; }; + // Get WebSocket URL with proper protocol + const getWebSocketUrl = () => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.host; + return `${protocol}//${host}`; + }; + + // Load pods list + const loadPods = async () => { + if (!deploymentId) return; + + setIsLoadingPods(true); + try { + const response = await api.get(`/v1/deployments/${deploymentId}/pods`); + setPods(response.data.pods || []); + } catch (err) { + console.error('Failed to load pods:', err); + // Don't set error for pods loading failure, just log it + } finally { + setIsLoadingPods(false); + } + }; + // Load historical logs from API const loadHistoricalLogs = async (retryCount = 0) => { if (!deploymentId) return; @@ -44,7 +70,14 @@ export default function LogsPage() { setError(null); try { - const response = await api.get(`/v1/deployments/${deploymentId}/logs?tail=100`); + let endpoint; + if (selectedPod === 'all') { + endpoint = `/v1/deployments/${deploymentId}/logs?tail=100`; + } else { + endpoint = `/v1/deployments/${deploymentId}/pods/${selectedPod}/logs?tail=100`; + } + + const response = await api.get(endpoint); if (response.data.logs) { // Parse historical logs - assuming they come as a single string with newlines @@ -62,7 +95,8 @@ export default function LogsPage() { timestamp, message: line, id: `history-${index}`, - isHistorical: true + isHistorical: true, + podName: selectedPod === 'all' ? 'merged' : selectedPod }; }); @@ -113,11 +147,22 @@ export default function LogsPage() { setIsConnecting(true); setError(null); - // Add token to WebSocket URL as query parameter - const wsUrl = `ws://localhost:3000/v1/deployments/${deploymentId}/logs/stream?tail=50&token=${encodeURIComponent('Bearer ' + token)}`; + // Build WebSocket URL based on selected pod with proper protocol + const baseWsUrl = getWebSocketUrl(); + let wsUrl; + + if (selectedPod === 'all') { + wsUrl = `${baseWsUrl}/v1/deployments/${deploymentId}/logs/stream?tail=50&token=${encodeURIComponent('Bearer ' + token)}`; + } else { + wsUrl = `${baseWsUrl}/v1/deployments/${deploymentId}/pods/${selectedPod}/logs/ws?tail=50&token=${encodeURIComponent('Bearer ' + token)}`; + } + + console.log('Connecting to WebSocket:', wsUrl.replace(/token=[^&]+/, 'token=***')); // Log URL without token + const ws = new WebSocket(wsUrl); ws.onopen = () => { + console.log('WebSocket connected'); setIsConnected(true); setIsConnecting(false); reconnectDelay.current = 1000; // Reset reconnect delay @@ -126,7 +171,9 @@ export default function LogsPage() { ws.onmessage = (event) => { // Skip connection confirmation messages if (event.data === 'Connected to log stream' || + event.data.includes('Connected to log stream for pod:') || event.data === 'Log stream ended' || + event.data.includes('Log stream ended for pod:') || event.data.includes('Authentication')) { return; } @@ -136,10 +183,11 @@ export default function LogsPage() { timestamp, message: event.data, id: `live-${timestamp}-${Math.random()}`, - isHistorical: false + isHistorical: false, + podName: selectedPod === 'all' ? 'merged' : selectedPod }; - setLogs((prev: any) => [...prev, newLog]); + setLogs(prev => [...prev, newLog]); }; ws.onerror = (error) => { @@ -148,6 +196,7 @@ export default function LogsPage() { }; ws.onclose = (event) => { + console.log(`WebSocket disconnected: ${event.code} ${event.reason || ''}`); setIsConnected(false); setIsConnecting(false); wsRef.current = null; @@ -168,6 +217,7 @@ export default function LogsPage() { const delay = reconnectDelay.current; reconnectDelay.current = Math.min(delay * 2, 30000); // Max 30s + console.log(`Attempting to reconnect in ${delay}ms (attempt ${Math.log2(delay / 1000) + 1})`); setError(`Disconnected. Reconnecting in ${delay / 1000}s...`); reconnectTimeoutRef.current = setTimeout(() => { @@ -178,9 +228,25 @@ export default function LogsPage() { wsRef.current = ws; }; - // Initialize: Load history first, then connect WebSocket + // Handle pod selection change + const handlePodChange = (podName: any) => { + if (podName === selectedPod) return; + + // Disconnect current WebSocket + if (wsRef.current) { + wsRef.current.close(); + } + + setSelectedPod(podName); + setLogs([]); // Clear current logs + }; + + // Initialize: Load pods and history first, then connect WebSocket useEffect(() => { if (deploymentId) { + // Load pods first + loadPods(); + // Load historical logs first loadHistoricalLogs().then(() => { // Small delay to show historical logs before connecting WebSocket @@ -201,6 +267,17 @@ export default function LogsPage() { }; }, [deploymentId]); + // Re-load logs when pod selection changes + useEffect(() => { + if (deploymentId && selectedPod) { + loadHistoricalLogs().then(() => { + setTimeout(() => { + connectWebSocket(); + }, 500); + }); + } + }, [selectedPod]); + // Manual refresh - reload everything const handleRefresh = async () => { setLogs([]); // Clear logs @@ -208,8 +285,8 @@ export default function LogsPage() { wsRef.current.close(); } - // Reload historical logs then reconnect WebSocket - await loadHistoricalLogs(); + // Reload pods and historical logs then reconnect WebSocket + await Promise.all([loadPods(), loadHistoricalLogs()]); setTimeout(() => { connectWebSocket(); }, 500); @@ -222,7 +299,7 @@ export default function LogsPage() { // Download logs const handleDownload = () => { - const logText = logs.map((log) => + const logText = logs.map(log => `[${new Date(log.timestamp).toLocaleString()}] ${log.message}` ).join('\n'); @@ -230,7 +307,8 @@ export default function LogsPage() { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `logs-${deploymentId}-${new Date().toISOString().split('T')[0]}.txt`; + const podSuffix = selectedPod === 'all' ? 'all-pods' : selectedPod; + a.download = `logs-${deploymentId}-${podSuffix}-${new Date().toISOString().split('T')[0]}.txt`; a.click(); URL.revokeObjectURL(url); }; @@ -264,7 +342,7 @@ export default function LogsPage() { return (
-
+
@@ -305,6 +383,44 @@ export default function LogsPage() {
+ {/* Pod Selection */} +
+
+ + View logs from: +
+
+ + {pods.map((pod: any) => ( + + ))} + {isLoadingPods && ( +
Loading pods...
+ )} +
+
+ {error && (
{error} @@ -316,7 +432,7 @@ export default function LogsPage() {
{logs.length > 0 ? ( <> - {logs.map((log) => ( + {logs.map(log => (
{new Date(log.timestamp).toLocaleTimeString()} @@ -324,6 +440,11 @@ export default function LogsPage() { {log.isHistorical ? '◦' : '│'} + {selectedPod === 'all' && log.podName !== 'merged' && ( + + [{log.podName}] + + )} {log.message}
))} @@ -336,7 +457,10 @@ export default function LogsPage() { {isLoadingHistory || isConnecting ? 'Loading logs...' : 'No logs available at the moment.'}

- Logs will appear here once your application starts generating them. + {selectedPod === 'all' + ? 'Logs from all pods will appear here once your application starts generating them.' + : `Logs from pod "${selectedPod}" will appear here once it starts generating them.` + }

)} @@ -360,6 +484,7 @@ export default function LogsPage() {
{logs.filter(log => log.isHistorical).length} historical + {logs.filter(log => !log.isHistorical).length} live logs + {selectedPod !== 'all' && ` from ${selectedPod}`} Total: {logs.length} lines
@@ -383,4 +508,4 @@ export default function LogsPage() { `}
); -} +} \ No newline at end of file diff --git a/apps/container-engine-frontend/src/components/Layout/DashboardLayout.tsx b/apps/container-engine-frontend/src/components/Layout/DashboardLayout.tsx index 3706dd2..1ea8dc4 100644 --- a/apps/container-engine-frontend/src/components/Layout/DashboardLayout.tsx +++ b/apps/container-engine-frontend/src/components/Layout/DashboardLayout.tsx @@ -49,6 +49,12 @@ const BellIcon = () => ( ); +const WebhookIcon = () => ( + + + +); + const MenuIcon = () => ( @@ -60,6 +66,7 @@ const navItems = [ { path: '/deployments', label: 'Deployments', icon: DeploymentIcon }, { path: '/deployments/new', label: 'New Deployment', icon: PlusIcon }, { path: '/api-keys', label: 'API Keys', icon: KeyIcon }, + { path: '/webhooks', label: 'Webhooks', icon: WebhookIcon }, { path: '/settings', label: 'Account Settings', icon: SettingsIcon }, ]; @@ -153,7 +160,7 @@ const Sidebar: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, o
{/* Logout Button */} -
+
@@ -423,8 +423,8 @@ const ApiKeysPage: React.FC = () => { {!loading && !error && ( <> {/* Stats Cards */} -
-
+
+
@@ -436,7 +436,7 @@ const ApiKeysPage: React.FC = () => {
-
+
@@ -448,7 +448,7 @@ const ApiKeysPage: React.FC = () => {
-
+
@@ -464,33 +464,33 @@ const ApiKeysPage: React.FC = () => { {/* Controls */} { apiKeys && apiKeys.length > 0 && ( -
-
- +
+
+ Showing {apiKeys.length > 0 ? ((currentPage - 1) * limit + 1) : 0} to {Math.min(currentPage * limit, total)} of {total} entries
-
- +
+ - entries + entries
) } {/* API Keys Table */} -
+
{total === 0 ? (
@@ -508,57 +508,54 @@ const ApiKeysPage: React.FC = () => {
) : ( <> -
-

Your API Keys

+
+

Your API Keys

- +
- - - - - - - + + + + + + + {apiKeys && apiKeys.length > 0 && apiKeys.map((key:any) => ( - - - -
NameKey IDDescriptionCreatedLast UsedExpiresActionsNameKey IDDescriptionCreatedLast UsedExpiresActions
-
-
+
+
+
- {key.name} + {key.name}
-
- - {maskKeyId(key.id)} - +
+
+ {maskKeyId(key.id)}
- + + {key.description || No description} +
{formatDate(key.created_at)} diff --git a/apps/container-engine-frontend/src/pages/AuthPage.tsx b/apps/container-engine-frontend/src/pages/AuthPage.tsx index cb40b2a..f4b09f1 100644 --- a/apps/container-engine-frontend/src/pages/AuthPage.tsx +++ b/apps/container-engine-frontend/src/pages/AuthPage.tsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom'; const AuthPage: React.FC = () => { const [isRegister, setIsRegister] = useState(false); + const [isForgotPassword, setIsForgotPassword] = useState(false); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -25,7 +26,12 @@ const AuthPage: React.FC = () => { setIsLoading(true); try { - if (isRegister) { + if (isForgotPassword) { + // Forgot password process + await api.post('/v1/auth/forgot-password', { email }); + setSuccess('If an account with that email exists, a password reset link has been sent to your email.'); + return; + } else if (isRegister) { if (password !== confirm_password) { setError('Passwords do not match.'); return; @@ -113,7 +119,9 @@ const AuthPage: React.FC = () => {
- {isRegister ? ( + {isForgotPassword ? ( + + ) : isRegister ? ( ) : ( @@ -121,10 +129,15 @@ const AuthPage: React.FC = () => {

- {isRegister ? 'Create Account' : 'Welcome Back'} + {isForgotPassword ? 'Reset Password' : isRegister ? 'Create Account' : 'Welcome Back'}

- {isRegister ? 'Join us and start your journey today' : 'Please sign in to your account'} + {isForgotPassword + ? 'Enter your email address and we\'ll send you a link to reset your password' + : isRegister + ? 'Join us and start your journey today' + : 'Please sign in to your account' + }

@@ -179,31 +192,33 @@ const AuthPage: React.FC = () => {
-
- -
- setPassword(e.target.value)} - /> - + {!isForgotPassword && ( +
+ +
+ setPassword(e.target.value)} + /> + +
-
+ )} {isRegister && (
@@ -262,7 +277,14 @@ const AuthPage: React.FC = () => {
) : ( - {isRegister ? ( + {isForgotPassword ? ( + <> + + + + Send Reset Link + + ) : isRegister ? ( <> @@ -297,19 +319,69 @@ const AuthPage: React.FC = () => { {/* Toggle */}

- {isRegister ? 'Already have an account?' : "Don't have an account?"}{' '} - + {isForgotPassword ? ( + <> + Remember your password?{' '} + + + ) : isRegister ? ( + <> + Already have an account?{' '} + + + ) : ( + <> + Don't have an account?{' '} + + + )}

+ {!isForgotPassword && !isRegister && ( +

+ Forgot your password?{' '} + +

+ )}
diff --git a/apps/container-engine-frontend/src/pages/DashboardPage.tsx b/apps/container-engine-frontend/src/pages/DashboardPage.tsx index 226f07d..0f24043 100644 --- a/apps/container-engine-frontend/src/pages/DashboardPage.tsx +++ b/apps/container-engine-frontend/src/pages/DashboardPage.tsx @@ -36,50 +36,50 @@ const DashboardPage: React.FC = () => { return ( -
-

Dashboard

- +
+

Dashboard

+ {loading && ( -
+
-

Loading stats...

+

Loading stats...

)} - + {error && ( -
-
+
+
-
-

{error}

+
+

{error}

)} {stats && ( -
-
-

Total Deployments

-

{stats.deploymentCount}

+
+
+

Total Deployments

+

{stats.deploymentCount}

Active containers

-
-

API Keys

-

{stats.apiKeyCount}

+
+

API Keys

+

{stats.apiKeyCount}

Authentication tokens

-
-

System Status

-
-
-

All Systems Operational

+
+

System Status

+
+
+

All Systems Operational

Last checked: {new Date().toLocaleTimeString()}

@@ -88,45 +88,45 @@ const DashboardPage: React.FC = () => { {/* Quick Actions */} {stats && ( -
-

Quick Actions

-
+ diff --git a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx index 7f1fb78..dd37a56 100644 --- a/apps/container-engine-frontend/src/pages/DocumentationPage.tsx +++ b/apps/container-engine-frontend/src/pages/DocumentationPage.tsx @@ -1,7 +1,7 @@ // src/pages/DocumentationPage.tsx import React, { useState } from 'react'; import { Link } from 'react-router-dom'; -import { +import { BookOpenIcon, RocketLaunchIcon, CodeBracketIcon, @@ -69,7 +69,7 @@ const DocumentationPage: React.FC = () => {