diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59aac2709..88cdf43eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,6 +189,7 @@ jobs: API_IMAGE_ID: ${{ needs.docker-images.outputs.api_image_id }} FRONTEND_IMAGE_ID: ${{ needs.docker-images.outputs.frontend_image_id }} TF_VAR_github_client_secret: ${{ secrets.GH_CLIENT_SECRET }} + TF_VAR_gitlab_client_secret: ${{ secrets.GITLAB_CLIENT_SECRET }} TF_VAR_postmark_token: ${{ secrets.POSTMARK_TOKEN }} TF_VAR_orama_package_index_id: ${{ secrets.ORAMA_PACKAGE_INDEX_ID }} TF_VAR_orama_symbols_index_id: ${{ secrets.ORAMA_SYMBOLS_INDEX_ID }} @@ -246,6 +247,7 @@ jobs: API_IMAGE_ID: ${{ needs.docker-images.outputs.api_image_id }} FRONTEND_IMAGE_ID: ${{ needs.docker-images.outputs.frontend_image_id }} TF_VAR_github_client_secret: ${{ secrets.GH_CLIENT_SECRET }} + TF_VAR_gitlab_client_secret: ${{ secrets.GITLAB_CLIENT_SECRET }} TF_VAR_postmark_token: ${{ secrets.POSTMARK_TOKEN }} TF_VAR_orama_package_index_id: ${{ secrets.ORAMA_PACKAGE_INDEX_ID }} TF_VAR_orama_symbols_index_id: ${{ secrets.ORAMA_SYMBOLS_INDEX_ID }} diff --git a/api/.env.example b/api/.env.example index 03a09ea4e..8f061272f 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,6 +1,8 @@ DATABASE_URL=postgres://@localhost/registry GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx +GITLAB_CLIENT_ID=xxx +GITLAB_CLIENT_SECRET=xxx GCS_ENDPOINT=http://localhost:4080 OTLP_ENDPOINT=http://localhost:4317 MODULES_BUCKET=modules diff --git a/api/.sqlx/query-bf4a6e0ab008dd6706e61538e41e29fa1429f706e438b0139b75d0fa555d296f.json b/api/.sqlx/query-0bfc9d9a83e668cd5ffc157500fdc808d8e1c3dbced139a2f2104296203d3cd2.json similarity index 78% rename from api/.sqlx/query-bf4a6e0ab008dd6706e61538e41e29fa1429f706e438b0139b75d0fa555d296f.json rename to api/.sqlx/query-0bfc9d9a83e668cd5ffc157500fdc808d8e1c3dbced139a2f2104296203d3cd2.json index d9d6a9dd9..830cb739d 100644 --- a/api/.sqlx/query-bf4a6e0ab008dd6706e61538e41e29fa1429f706e438b0139b75d0fa555d296f.json +++ b/api/.sqlx/query-0bfc9d9a83e668cd5ffc157500fdc808d8e1c3dbced139a2f2104296203d3cd2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n ticket_messages.ticket_id as \"message_ticket_id\",\n ticket_messages.author as \"message_author\",\n ticket_messages.message as \"message_message\",\n ticket_messages.updated_at as \"message_updated_at\",\n ticket_messages.created_at as \"message_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\"\n FROM ticket_messages\n LEFT JOIN users ON users.id = ticket_messages.author\n WHERE ticket_messages.ticket_id = $1 ORDER BY ticket_messages.created_at", + "query": "SELECT\n ticket_messages.ticket_id as \"message_ticket_id\",\n ticket_messages.author as \"message_author\",\n ticket_messages.message as \"message_message\",\n ticket_messages.updated_at as \"message_updated_at\",\n ticket_messages.created_at as \"message_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\"\n FROM ticket_messages\n INNER JOIN users ON users.id = ticket_messages.author\n WHERE ticket_messages.ticket_id = $1 ORDER BY ticket_messages.created_at", "describe": { "columns": [ { @@ -50,11 +50,16 @@ }, { "ordinal": 9, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 10, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "user_created_at", "type_info": "Timestamptz" } @@ -74,9 +79,10 @@ false, false, true, + true, false, false ] }, - "hash": "bf4a6e0ab008dd6706e61538e41e29fa1429f706e438b0139b75d0fa555d296f" + "hash": "0bfc9d9a83e668cd5ffc157500fdc808d8e1c3dbced139a2f2104296203d3cd2" } diff --git a/api/.sqlx/query-be213e3a3855c8e950320d4f8fcf6c8e1879c81e912c762ae402d6a7411e86a6.json b/api/.sqlx/query-0cf3cd18611c35f411581f89652bfd9420f6540ca4cfec7c258bb33bc93b36f0.json similarity index 88% rename from api/.sqlx/query-be213e3a3855c8e950320d4f8fcf6c8e1879c81e912c762ae402d6a7411e86a6.json rename to api/.sqlx/query-0cf3cd18611c35f411581f89652bfd9420f6540ca4cfec7c258bb33bc93b36f0.json index c36c8f32d..c9ad3056d 100644 --- a/api/.sqlx/query-be213e3a3855c8e950320d4f8fcf6c8e1879c81e912c762ae402d6a7411e86a6.json +++ b/api/.sqlx/query-0cf3cd18611c35f411581f89652bfd9420f6540ca4cfec7c258bb33bc93b36f0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT package_versions.scope as \"package_version_scope: ScopeName\", package_versions.name as \"package_version_name: PackageName\", package_versions.version as \"package_version_version: Version\", package_versions.user_id as \"package_version_user_id\", package_versions.readme_path as \"package_version_readme_path: PackagePath\", package_versions.exports as \"package_version_exports: ExportsMap\", package_versions.is_yanked as \"package_version_is_yanked\", package_versions.uses_npm as \"package_version_uses_npm\", package_versions.meta as \"package_version_meta: PackageVersionMeta\", package_versions.updated_at as \"package_version_updated_at\", package_versions.created_at as \"package_version_created_at\", package_versions.rekor_log_id as \"package_version_rekor_log_id\",\n (SELECT COUNT(*)\n FROM package_versions AS pv\n WHERE pv.scope = package_versions.scope\n AND pv.name = package_versions.name\n AND pv.version > package_versions.version\n AND pv.version NOT LIKE '%-%'\n AND pv.is_yanked = false) as \"package_version_newer_versions_count!\",\n (SELECT COALESCE(SUM(dl.count), 0)\n FROM version_download_counts_24h as dl\n WHERE dl.scope = package_versions.scope\n AND dl.package = package_versions.name\n AND dl.version = package_versions.version) as \"package_version_lifetime_download_count!\",\n users.id as \"user_id?\", users.name as \"user_name?\", users.avatar_url as \"user_avatar_url?\", users.github_id as \"user_github_id\", users.updated_at as \"user_updated_at?\", users.created_at as \"user_created_at?\"\n FROM package_versions\n LEFT JOIN users ON package_versions.user_id = users.id\n WHERE package_versions.scope = $1 AND package_versions.name = $2\n ORDER BY package_versions.version DESC", + "query": "SELECT package_versions.scope as \"package_version_scope: ScopeName\", package_versions.name as \"package_version_name: PackageName\", package_versions.version as \"package_version_version: Version\", package_versions.user_id as \"package_version_user_id\", package_versions.readme_path as \"package_version_readme_path: PackagePath\", package_versions.exports as \"package_version_exports: ExportsMap\", package_versions.is_yanked as \"package_version_is_yanked\", package_versions.uses_npm as \"package_version_uses_npm\", package_versions.meta as \"package_version_meta: PackageVersionMeta\", package_versions.updated_at as \"package_version_updated_at\", package_versions.created_at as \"package_version_created_at\", package_versions.rekor_log_id as \"package_version_rekor_log_id\",\n (SELECT COUNT(*)\n FROM package_versions AS pv\n WHERE pv.scope = package_versions.scope\n AND pv.name = package_versions.name\n AND pv.version > package_versions.version\n AND pv.version NOT LIKE '%-%'\n AND pv.is_yanked = false) as \"package_version_newer_versions_count!\",\n (SELECT COALESCE(SUM(dl.count), 0)\n FROM version_download_counts_24h as dl\n WHERE dl.scope = package_versions.scope\n AND dl.package = package_versions.name\n AND dl.version = package_versions.version) as \"package_version_lifetime_download_count!\",\n users.id as \"user_id?\", users.name as \"user_name?\", users.avatar_url as \"user_avatar_url?\", users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\", users.updated_at as \"user_updated_at?\", users.created_at as \"user_created_at?\"\n FROM package_versions\n LEFT JOIN users ON package_versions.user_id = users.id\n WHERE package_versions.scope = $1 AND package_versions.name = $2\n ORDER BY package_versions.version DESC", "describe": { "columns": [ { @@ -95,11 +95,16 @@ }, { "ordinal": 18, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 19, "name": "user_updated_at?", "type_info": "Timestamptz" }, { - "ordinal": 19, + "ordinal": 20, "name": "user_created_at?", "type_info": "Timestamptz" } @@ -129,9 +134,10 @@ false, false, true, + true, false, false ] }, - "hash": "be213e3a3855c8e950320d4f8fcf6c8e1879c81e912c762ae402d6a7411e86a6" + "hash": "0cf3cd18611c35f411581f89652bfd9420f6540ca4cfec7c258bb33bc93b36f0" } diff --git a/api/.sqlx/query-f24bbdd8a0f8e2d492ac333aaef326ee42b59993673756ce00012eca8906986c.json b/api/.sqlx/query-0d8772210f861b434fd075b1569afeb4726d825e2740737bdad8de0236d97af2.json similarity index 64% rename from api/.sqlx/query-f24bbdd8a0f8e2d492ac333aaef326ee42b59993673756ce00012eca8906986c.json rename to api/.sqlx/query-0d8772210f861b434fd075b1569afeb4726d825e2740737bdad8de0236d97af2.json index 81aa8ff99..8c52b3087 100644 --- a/api/.sqlx/query-f24bbdd8a0f8e2d492ac333aaef326ee42b59993673756ce00012eca8906986c.json +++ b/api/.sqlx/query-0d8772210f861b434fd075b1569afeb4726d825e2740737bdad8de0236d97af2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n tickets.id as \"ticket_id\",\n tickets.kind as \"ticket_kind: TicketKind\",\n tickets.creator as \"ticket_creator\",\n tickets.meta as \"ticket_meta\",\n tickets.closed as \"ticket_closed\",\n tickets.updated_at as \"ticket_updated_at\",\n tickets.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM tickets\n INNER JOIN users ON users.id = tickets.creator\n WHERE tickets.creator = $1\n ORDER BY tickets.closed ASC, tickets.created_at DESC\n", + "query": "SELECT\n tickets.id as \"ticket_id\",\n tickets.kind as \"ticket_kind: TicketKind\",\n tickets.creator as \"ticket_creator\",\n tickets.meta as \"ticket_meta\",\n tickets.closed as \"ticket_closed\",\n tickets.updated_at as \"ticket_updated_at\",\n tickets.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM tickets\n INNER JOIN users ON users.id = tickets.creator\n WHERE tickets.creator = $1\n ORDER BY tickets.closed ASC, tickets.created_at DESC\n", "describe": { "columns": [ { @@ -78,41 +78,46 @@ }, { "ordinal": 12, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 13, "name": "user_is_blocked", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 14, "name": "user_is_staff", "type_info": "Bool" }, { - "ordinal": 14, + "ordinal": 15, "name": "user_scope_limit", "type_info": "Int4" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 17, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 17, + "ordinal": 18, "name": "user_invite_count!", "type_info": "Int8" }, { - "ordinal": 18, + "ordinal": 19, "name": "user_scope_usage!", "type_info": "Int8" }, { - "ordinal": 19, + "ordinal": 20, "name": "user_newer_ticket_messages_count!", "type_info": "Int8" } @@ -135,6 +140,7 @@ true, false, true, + true, false, false, false, @@ -145,5 +151,5 @@ null ] }, - "hash": "f24bbdd8a0f8e2d492ac333aaef326ee42b59993673756ce00012eca8906986c" + "hash": "0d8772210f861b434fd075b1569afeb4726d825e2740737bdad8de0236d97af2" } diff --git a/api/.sqlx/query-788ca9ad465bc3ebd8f5ea2e0134905b2dbe2aea6c482962a99ea2b804a42268.json b/api/.sqlx/query-13c9c323fa40663648962e43018e2e6f85fda27a7f12b572f18e03146c8f741b.json similarity index 84% rename from api/.sqlx/query-788ca9ad465bc3ebd8f5ea2e0134905b2dbe2aea6c482962a99ea2b804a42268.json rename to api/.sqlx/query-13c9c323fa40663648962e43018e2e6f85fda27a7f12b572f18e03146c8f741b.json index 00acc16c2..d920ab41a 100644 --- a/api/.sqlx/query-788ca9ad465bc3ebd8f5ea2e0134905b2dbe2aea6c482962a99ea2b804a42268.json +++ b/api/.sqlx/query-13c9c323fa40663648962e43018e2e6f85fda27a7f12b572f18e03146c8f741b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n publishing_tasks.id as \"task_id\",\n publishing_tasks.status as \"task_status: PublishingTaskStatus\",\n publishing_tasks.error as \"task_error: PublishingTaskError\",\n publishing_tasks.user_id as \"task_user_id\",\n publishing_tasks.package_scope as \"task_package_scope: ScopeName\",\n publishing_tasks.package_name as \"task_package_name: PackageName\",\n publishing_tasks.package_version as \"task_package_version: Version\",\n publishing_tasks.config_file as \"task_config_file: PackagePath\",\n publishing_tasks.created_at as \"task_created_at\",\n publishing_tasks.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM publishing_tasks\n LEFT JOIN users on publishing_tasks.user_id = users.id\n WHERE package_scope = $1 AND package_name = $2 AND package_version = $3 AND status != 'failure'\n LIMIT 1", + "query": "SELECT\n publishing_tasks.id as \"task_id\",\n publishing_tasks.status as \"task_status: PublishingTaskStatus\",\n publishing_tasks.error as \"task_error: PublishingTaskError\",\n publishing_tasks.user_id as \"task_user_id\",\n publishing_tasks.package_scope as \"task_package_scope: ScopeName\",\n publishing_tasks.package_name as \"task_package_name: PackageName\",\n publishing_tasks.package_version as \"task_package_version: Version\",\n publishing_tasks.config_file as \"task_config_file: PackagePath\",\n publishing_tasks.created_at as \"task_created_at\",\n publishing_tasks.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\nusers.gitlab_id as \"user_gitlab_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM publishing_tasks\n LEFT JOIN users on publishing_tasks.user_id = users.id\n WHERE package_scope = $1 AND package_name = $2 AND package_version = $3 AND status != 'failure'\n LIMIT 1", "describe": { "columns": [ { @@ -88,11 +88,16 @@ }, { "ordinal": 14, + "name": "user_gitlab_id?", + "type_info": "Int8" + }, + { + "ordinal": 15, "name": "user_updated_at?", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_created_at?", "type_info": "Timestamptz" } @@ -119,9 +124,10 @@ false, false, true, + true, false, false ] }, - "hash": "788ca9ad465bc3ebd8f5ea2e0134905b2dbe2aea6c482962a99ea2b804a42268" + "hash": "13c9c323fa40663648962e43018e2e6f85fda27a7f12b572f18e03146c8f741b" } diff --git a/api/.sqlx/query-103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b.json b/api/.sqlx/query-1dcad900a28c7e4444c89b8cd1462922eab0694ec47e13e99968d94133558bf2.json similarity index 85% rename from api/.sqlx/query-103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b.json rename to api/.sqlx/query-1dcad900a28c7e4444c89b8cd1462922eab0694ec47e13e99968d94133558bf2.json index 2d3ecd6de..2cb6a0e50 100644 --- a/api/.sqlx/query-103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b.json +++ b/api/.sqlx/query-1dcad900a28c7e4444c89b8cd1462922eab0694ec47e13e99968d94133558bf2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH usage AS (\n SELECT\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1) AS package,\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1 AND created_at > now() - '1 week'::interval) AS new_package_per_week,\n (SELECT COUNT(created_at) FROM publishing_tasks WHERE package_scope = $1 AND created_at > now() - '1 week'::interval) AS publish_attempts_per_week\n )\n SELECT\n scopes.scope as \"scope_scope: ScopeName\",\n scopes.description as \"scope_description: ScopeDescription\",\n scopes.creator as \"scope_creator\",\n scopes.package_limit as \"scope_package_limit\",\n scopes.new_package_per_week_limit as \"scope_new_package_per_week_limit\",\n scopes.publish_attempts_per_week_limit as \"scope_publish_attempts_per_week_limit\",\n scopes.verify_oidc_actor as \"scope_verify_oidc_actor\",\n scopes.require_publishing_from_ci as \"scope_require_publishing_from_ci\",\n scopes.updated_at as \"scope_updated_at\",\n scopes.created_at as \"scope_created_at\",\n users.id as \"user_id\", users.name as \"user_name\", users.avatar_url as \"user_avatar_url\", users.github_id as \"user_github_id\", users.updated_at as \"user_updated_at\", users.created_at as \"user_created_at\",\n usage.package as \"usage_package\", usage.new_package_per_week as \"usage_new_package_per_week\", usage.publish_attempts_per_week as \"usage_publish_attempts_per_week\"\n FROM scopes\n LEFT JOIN users ON scopes.creator = users.id\n CROSS JOIN usage\n WHERE scopes.scope = $1\n ", + "query": "\n WITH usage AS (\n SELECT\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1) AS package,\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1 AND created_at > now() - '1 week'::interval) AS new_package_per_week,\n (SELECT COUNT(created_at) FROM publishing_tasks WHERE package_scope = $1 AND created_at > now() - '1 week'::interval) AS publish_attempts_per_week\n )\n SELECT\n scopes.scope as \"scope_scope: ScopeName\",\n scopes.description as \"scope_description: ScopeDescription\",\n scopes.creator as \"scope_creator\",\n scopes.package_limit as \"scope_package_limit\",\n scopes.new_package_per_week_limit as \"scope_new_package_per_week_limit\",\n scopes.publish_attempts_per_week_limit as \"scope_publish_attempts_per_week_limit\",\n scopes.verify_oidc_actor as \"scope_verify_oidc_actor\",\n scopes.require_publishing_from_ci as \"scope_require_publishing_from_ci\",\n scopes.updated_at as \"scope_updated_at\",\n scopes.created_at as \"scope_created_at\",\n users.id as \"user_id\", users.name as \"user_name\", users.avatar_url as \"user_avatar_url\", users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\", users.updated_at as \"user_updated_at\", users.created_at as \"user_created_at\",\n usage.package as \"usage_package\", usage.new_package_per_week as \"usage_new_package_per_week\", usage.publish_attempts_per_week as \"usage_publish_attempts_per_week\"\n FROM scopes\n LEFT JOIN users ON scopes.creator = users.id\n CROSS JOIN usage\n WHERE scopes.scope = $1\n ", "describe": { "columns": [ { @@ -75,26 +75,31 @@ }, { "ordinal": 14, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 15, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 17, "name": "usage_package", "type_info": "Int8" }, { - "ordinal": 17, + "ordinal": 18, "name": "usage_new_package_per_week", "type_info": "Int8" }, { - "ordinal": 18, + "ordinal": 19, "name": "usage_publish_attempts_per_week", "type_info": "Int8" } @@ -119,6 +124,7 @@ false, false, true, + true, false, false, null, @@ -126,5 +132,5 @@ null ] }, - "hash": "103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b" + "hash": "1dcad900a28c7e4444c89b8cd1462922eab0694ec47e13e99968d94133558bf2" } diff --git a/api/.sqlx/query-2f6e7e603f38e69d37ac66c575553b260c311898e7937bb0c08e0740a76aee88.json b/api/.sqlx/query-2f76fb48f0b80135a8fffee61d691ec190ee8f2e500098edea11d2c8575feb01.json similarity index 81% rename from api/.sqlx/query-2f6e7e603f38e69d37ac66c575553b260c311898e7937bb0c08e0740a76aee88.json rename to api/.sqlx/query-2f76fb48f0b80135a8fffee61d691ec190ee8f2e500098edea11d2c8575feb01.json index c9ec8885b..cdf8c1f15 100644 --- a/api/.sqlx/query-2f6e7e603f38e69d37ac66c575553b260c311898e7937bb0c08e0740a76aee88.json +++ b/api/.sqlx/query-2f76fb48f0b80135a8fffee61d691ec190ee8f2e500098edea11d2c8575feb01.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH message AS (\n INSERT INTO ticket_messages (ticket_id, author, message)\n VALUES ($1, $2, $3)\n RETURNING ticket_id, author, message, updated_at, created_at\n )\n SELECT\n message.ticket_id as \"message_ticket_id\",\n message.author as \"message_author\",\n message.message as \"message_message\",\n message.updated_at as \"message_updated_at\",\n message.created_at as \"message_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\"\n FROM message\n INNER JOIN users ON users.id = message.author\n ", + "query": "WITH message AS (\n INSERT INTO ticket_messages (ticket_id, author, message)\n VALUES ($1, $2, $3)\n RETURNING ticket_id, author, message, updated_at, created_at\n )\n SELECT\n message.ticket_id as \"message_ticket_id\",\n message.author as \"message_author\",\n message.message as \"message_message\",\n message.updated_at as \"message_updated_at\",\n message.created_at as \"message_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\"\n FROM message\n INNER JOIN users ON users.id = message.author\n ", "describe": { "columns": [ { @@ -50,11 +50,16 @@ }, { "ordinal": 9, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 10, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "user_created_at", "type_info": "Timestamptz" } @@ -76,9 +81,10 @@ false, false, true, + true, false, false ] }, - "hash": "2f6e7e603f38e69d37ac66c575553b260c311898e7937bb0c08e0740a76aee88" + "hash": "2f76fb48f0b80135a8fffee61d691ec190ee8f2e500098edea11d2c8575feb01" } diff --git a/api/.sqlx/query-93da3d9967af3481cc488eca6a546a99a64d359856d219973d0ef587ff131ae0.json b/api/.sqlx/query-40c74b4755623895f7879e681bad6957f78870cdaeefb8c77d1cc418fdad10cd.json similarity index 54% rename from api/.sqlx/query-93da3d9967af3481cc488eca6a546a99a64d359856d219973d0ef587ff131ae0.json rename to api/.sqlx/query-40c74b4755623895f7879e681bad6957f78870cdaeefb8c77d1cc418fdad10cd.json index 4c6289820..4dfc12ba2 100644 --- a/api/.sqlx/query-93da3d9967af3481cc488eca6a546a99a64d359856d219973d0ef587ff131ae0.json +++ b/api/.sqlx/query-40c74b4755623895f7879e681bad6957f78870cdaeefb8c77d1cc418fdad10cd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n FROM users\n WHERE github_id = $1", + "query": "SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n FROM users\n WHERE github_id = $1", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -82,6 +87,7 @@ false, false, true, + true, false, false, false, @@ -90,5 +96,5 @@ null ] }, - "hash": "93da3d9967af3481cc488eca6a546a99a64d359856d219973d0ef587ff131ae0" + "hash": "40c74b4755623895f7879e681bad6957f78870cdaeefb8c77d1cc418fdad10cd" } diff --git a/api/.sqlx/query-af89de0d5bb408bd4b55252bb9c6266ae066846f97486bc6407793f1c6eaef9a.json b/api/.sqlx/query-50f749d2b65f10872cce1051ccbb9600ae971e1fbe74bee593d708778ea08112.json similarity index 64% rename from api/.sqlx/query-af89de0d5bb408bd4b55252bb9c6266ae066846f97486bc6407793f1c6eaef9a.json rename to api/.sqlx/query-50f749d2b65f10872cce1051ccbb9600ae971e1fbe74bee593d708778ea08112.json index e375e942d..d9f895c3f 100644 --- a/api/.sqlx/query-af89de0d5bb408bd4b55252bb9c6266ae066846f97486bc6407793f1c6eaef9a.json +++ b/api/.sqlx/query-50f749d2b65f10872cce1051ccbb9600ae971e1fbe74bee593d708778ea08112.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n tickets.id as \"ticket_id\",\n tickets.kind as \"ticket_kind: TicketKind\",\n tickets.creator as \"ticket_creator\",\n tickets.meta as \"ticket_meta\",\n tickets.closed as \"ticket_closed\",\n tickets.updated_at as \"ticket_updated_at\",\n tickets.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM tickets\n INNER JOIN users ON users.id = tickets.creator\n WHERE tickets.id = $1", + "query": "SELECT\n tickets.id as \"ticket_id\",\n tickets.kind as \"ticket_kind: TicketKind\",\n tickets.creator as \"ticket_creator\",\n tickets.meta as \"ticket_meta\",\n tickets.closed as \"ticket_closed\",\n tickets.updated_at as \"ticket_updated_at\",\n tickets.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.gitlab_id as \"user_gitlab_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM tickets\n INNER JOIN users ON users.id = tickets.creator\n WHERE tickets.id = $1", "describe": { "columns": [ { @@ -78,41 +78,46 @@ }, { "ordinal": 12, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 13, "name": "user_is_blocked", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 14, "name": "user_is_staff", "type_info": "Bool" }, { - "ordinal": 14, + "ordinal": 15, "name": "user_scope_limit", "type_info": "Int4" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 17, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 17, + "ordinal": 18, "name": "user_invite_count!", "type_info": "Int8" }, { - "ordinal": 18, + "ordinal": 19, "name": "user_scope_usage!", "type_info": "Int8" }, { - "ordinal": 19, + "ordinal": 20, "name": "user_newer_ticket_messages_count!", "type_info": "Int8" } @@ -135,6 +140,7 @@ true, false, true, + true, false, false, false, @@ -145,5 +151,5 @@ null ] }, - "hash": "af89de0d5bb408bd4b55252bb9c6266ae066846f97486bc6407793f1c6eaef9a" + "hash": "50f749d2b65f10872cce1051ccbb9600ae971e1fbe74bee593d708778ea08112" } diff --git a/api/.sqlx/query-8a1206beba880dec3cceacd228100ddef9a9151bfa03cccf9107c1e9432f4e92.json b/api/.sqlx/query-56c984b87e52e8588246bf3e9fcdba43ad66e4707989b66105c9b892367fe196.json similarity index 78% rename from api/.sqlx/query-8a1206beba880dec3cceacd228100ddef9a9151bfa03cccf9107c1e9432f4e92.json rename to api/.sqlx/query-56c984b87e52e8588246bf3e9fcdba43ad66e4707989b66105c9b892367fe196.json index 1e45cd474..97b3ae263 100644 --- a/api/.sqlx/query-8a1206beba880dec3cceacd228100ddef9a9151bfa03cccf9107c1e9432f4e92.json +++ b/api/.sqlx/query-56c984b87e52e8588246bf3e9fcdba43ad66e4707989b66105c9b892367fe196.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT scope_members.scope as \"scope_member_scope: ScopeName\", scope_members.user_id as \"scope_member_user_id\", scope_members.is_admin as \"scope_member_is_admin\", scope_members.updated_at as \"scope_member_updated_at\", scope_members.created_at as \"scope_member_created_at\",\n users.id as \"user_id\", users.name as \"user_name\", users.avatar_url as \"user_avatar_url\", users.github_id as \"user_github_id\", users.updated_at as \"user_updated_at\", users.created_at as \"user_created_at\"\n FROM scope_members\n LEFT JOIN users ON scope_members.user_id = users.id\n WHERE scope = $1\n ORDER BY users.name ASC", + "query": "SELECT scope_members.scope as \"scope_member_scope: ScopeName\", scope_members.user_id as \"scope_member_user_id\", scope_members.is_admin as \"scope_member_is_admin\", scope_members.updated_at as \"scope_member_updated_at\", scope_members.created_at as \"scope_member_created_at\",\n users.id as \"user_id\", users.name as \"user_name\", users.avatar_url as \"user_avatar_url\", users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\", users.updated_at as \"user_updated_at\", users.created_at as \"user_created_at\"\n FROM scope_members\n LEFT JOIN users ON scope_members.user_id = users.id\n WHERE scope = $1\n ORDER BY users.name ASC", "describe": { "columns": [ { @@ -50,11 +50,16 @@ }, { "ordinal": 9, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 10, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "user_created_at", "type_info": "Timestamptz" } @@ -74,9 +79,10 @@ false, false, true, + true, false, false ] }, - "hash": "8a1206beba880dec3cceacd228100ddef9a9151bfa03cccf9107c1e9432f4e92" + "hash": "56c984b87e52e8588246bf3e9fcdba43ad66e4707989b66105c9b892367fe196" } diff --git a/api/.sqlx/query-b95428de974f0d2febf52916b469f964837fe0e8ffabc11a18cef1f15279ce85.json b/api/.sqlx/query-7907d524a50097679dc6d0cf79be3f2fca1210846b0d1df3489e38c3f962ddb1.json similarity index 72% rename from api/.sqlx/query-b95428de974f0d2febf52916b469f964837fe0e8ffabc11a18cef1f15279ce85.json rename to api/.sqlx/query-7907d524a50097679dc6d0cf79be3f2fca1210846b0d1df3489e38c3f962ddb1.json index 8fd858ec6..d590708c9 100644 --- a/api/.sqlx/query-b95428de974f0d2febf52916b469f964837fe0e8ffabc11a18cef1f15279ce85.json +++ b/api/.sqlx/query-7907d524a50097679dc6d0cf79be3f2fca1210846b0d1df3489e38c3f962ddb1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, avatar_url, github_id, updated_at, created_at\n FROM users\n WHERE id = $1", + "query": "SELECT id, name, avatar_url, github_id, gitlab_id, updated_at, created_at\n FROM users\n WHERE id = $1", "describe": { "columns": [ { @@ -25,11 +25,16 @@ }, { "ordinal": 4, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 5, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 5, + "ordinal": 6, "name": "created_at", "type_info": "Timestamptz" } @@ -44,9 +49,10 @@ false, false, true, + true, false, false ] }, - "hash": "b95428de974f0d2febf52916b469f964837fe0e8ffabc11a18cef1f15279ce85" + "hash": "7907d524a50097679dc6d0cf79be3f2fca1210846b0d1df3489e38c3f962ddb1" } diff --git a/api/.sqlx/query-1fadf96624f1f748dd11def0e18c83e3f326f8e2cf67849ecfda6aa75fcc5d0e.json b/api/.sqlx/query-7ff1356bca82c551f4c75d5af4f7a1b56c210f04472c7deb5e540cb2794083f6.json similarity index 87% rename from api/.sqlx/query-1fadf96624f1f748dd11def0e18c83e3f326f8e2cf67849ecfda6aa75fcc5d0e.json rename to api/.sqlx/query-7ff1356bca82c551f4c75d5af4f7a1b56c210f04472c7deb5e540cb2794083f6.json index 6f6f97bcd..f2d7e38f5 100644 --- a/api/.sqlx/query-1fadf96624f1f748dd11def0e18c83e3f326f8e2cf67849ecfda6aa75fcc5d0e.json +++ b/api/.sqlx/query-7ff1356bca82c551f4c75d5af4f7a1b56c210f04472c7deb5e540cb2794083f6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO users (name, email, avatar_url, github_id, is_blocked, is_staff)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT(github_id) DO UPDATE\n SET name = $1, email = $2, avatar_url = $3\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "query": "INSERT INTO users (name, email, avatar_url, github_id, gitlab_id, is_blocked, is_staff)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -75,6 +80,7 @@ "Varchar", "Text", "Int8", + "Int8", "Bool", "Bool" ] @@ -87,6 +93,7 @@ false, false, true, + true, false, false, false, @@ -95,5 +102,5 @@ null ] }, - "hash": "1fadf96624f1f748dd11def0e18c83e3f326f8e2cf67849ecfda6aa75fcc5d0e" + "hash": "7ff1356bca82c551f4c75d5af4f7a1b56c210f04472c7deb5e540cb2794083f6" } diff --git a/api/.sqlx/query-8d901d30bdfd9b3c2ad4f4740bcd92591994543757632080a5b0628322492dec.json b/api/.sqlx/query-8d901d30bdfd9b3c2ad4f4740bcd92591994543757632080a5b0628322492dec.json new file mode 100644 index 000000000..556ca3195 --- /dev/null +++ b/api/.sqlx/query-8d901d30bdfd9b3c2ad4f4740bcd92591994543757632080a5b0628322492dec.json @@ -0,0 +1,101 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET gitlab_id = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "avatar_url", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "is_blocked", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "is_staff", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "scope_limit", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "invite_count!", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "scope_usage!", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "newer_ticket_messages_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "8d901d30bdfd9b3c2ad4f4740bcd92591994543757632080a5b0628322492dec" +} diff --git a/api/.sqlx/query-1ac786d2538ed726d9f1b723550bf7c8b6df3bfabaa03a1a4f4f1808ac12042b.json b/api/.sqlx/query-902c97a1abe0ec5a6546d9b8dff35ee76ae7326d5582add97c697c0c5a8738e8.json similarity index 54% rename from api/.sqlx/query-1ac786d2538ed726d9f1b723550bf7c8b6df3bfabaa03a1a4f4f1808ac12042b.json rename to api/.sqlx/query-902c97a1abe0ec5a6546d9b8dff35ee76ae7326d5582add97c697c0c5a8738e8.json index b3bc72fd8..bf8b52f63 100644 --- a/api/.sqlx/query-1ac786d2538ed726d9f1b723550bf7c8b6df3bfabaa03a1a4f4f1808ac12042b.json +++ b/api/.sqlx/query-902c97a1abe0ec5a6546d9b8dff35ee76ae7326d5582add97c697c0c5a8738e8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n FROM users\n WHERE id = $1", + "query": "SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n FROM users\n WHERE id = $1", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -82,6 +87,7 @@ false, false, true, + true, false, false, false, @@ -90,5 +96,5 @@ null ] }, - "hash": "1ac786d2538ed726d9f1b723550bf7c8b6df3bfabaa03a1a4f4f1808ac12042b" + "hash": "902c97a1abe0ec5a6546d9b8dff35ee76ae7326d5582add97c697c0c5a8738e8" } diff --git a/api/.sqlx/query-99129732598ec2104db096e0745ff681f078ce0a8f200c005282f5481ce688a7.json b/api/.sqlx/query-99129732598ec2104db096e0745ff681f078ce0a8f200c005282f5481ce688a7.json new file mode 100644 index 000000000..9f9253abf --- /dev/null +++ b/api/.sqlx/query-99129732598ec2104db096e0745ff681f078ce0a8f200c005282f5481ce688a7.json @@ -0,0 +1,55 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO gitlab_identities (gitlab_id, access_token, access_token_expires_at, refresh_token) VALUES ($1, $2, $3, $4)\n ON CONFLICT (gitlab_id) DO\n UPDATE SET access_token = $2, access_token_expires_at = $3, refresh_token = $4\n RETURNING gitlab_id, access_token, access_token_expires_at, refresh_token, updated_at, created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "access_token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "access_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "refresh_token", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Timestamptz", + "Text" + ] + }, + "nullable": [ + false, + true, + true, + true, + false, + false + ] + }, + "hash": "99129732598ec2104db096e0745ff681f078ce0a8f200c005282f5481ce688a7" +} diff --git a/api/.sqlx/query-8420f69ececdcfd2d09fc8f789012f02b08aaa5a3b612df2dfbdf0560d4d6a0d.json b/api/.sqlx/query-9c70be15a66c152db68afa2c7726ed82cf2fcb73a5d07729e9285be1c3331f46.json similarity index 66% rename from api/.sqlx/query-8420f69ececdcfd2d09fc8f789012f02b08aaa5a3b612df2dfbdf0560d4d6a0d.json rename to api/.sqlx/query-9c70be15a66c152db68afa2c7726ed82cf2fcb73a5d07729e9285be1c3331f46.json index ea0af7bec..d04c9593b 100644 --- a/api/.sqlx/query-8420f69ececdcfd2d09fc8f789012f02b08aaa5a3b612df2dfbdf0560d4d6a0d.json +++ b/api/.sqlx/query-9c70be15a66c152db68afa2c7726ed82cf2fcb73a5d07729e9285be1c3331f46.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH ticket AS (\n UPDATE tickets SET closed = $1 WHERE id = $2\n RETURNING id, kind, creator, meta, closed, updated_at, created_at\n )\n SELECT\n ticket.id as \"ticket_id\",\n ticket.kind as \"ticket_kind: TicketKind\",\n ticket.creator as \"ticket_creator\",\n ticket.meta as \"ticket_meta\",\n ticket.closed as \"ticket_closed\",\n ticket.updated_at as \"ticket_updated_at\",\n ticket.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM ticket\n INNER JOIN users ON users.id = ticket.creator\n ", + "query": "WITH ticket AS (\n UPDATE tickets SET closed = $1 WHERE id = $2\n RETURNING id, kind, creator, meta, closed, updated_at, created_at\n )\n SELECT\n ticket.id as \"ticket_id\",\n ticket.kind as \"ticket_kind: TicketKind\",\n ticket.creator as \"ticket_creator\",\n ticket.meta as \"ticket_meta\",\n ticket.closed as \"ticket_closed\",\n ticket.updated_at as \"ticket_updated_at\",\n ticket.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM ticket\n INNER JOIN users ON users.id = ticket.creator\n ", "describe": { "columns": [ { @@ -78,41 +78,46 @@ }, { "ordinal": 12, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 13, "name": "user_is_blocked", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 14, "name": "user_is_staff", "type_info": "Bool" }, { - "ordinal": 14, + "ordinal": 15, "name": "user_scope_limit", "type_info": "Int4" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 17, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 17, + "ordinal": 18, "name": "user_invite_count!", "type_info": "Int8" }, { - "ordinal": 18, + "ordinal": 19, "name": "user_scope_usage!", "type_info": "Int8" }, { - "ordinal": 19, + "ordinal": 20, "name": "user_newer_ticket_messages_count!", "type_info": "Int8" } @@ -136,6 +141,7 @@ true, false, true, + true, false, false, false, @@ -146,5 +152,5 @@ null ] }, - "hash": "8420f69ececdcfd2d09fc8f789012f02b08aaa5a3b612df2dfbdf0560d4d6a0d" + "hash": "9c70be15a66c152db68afa2c7726ed82cf2fcb73a5d07729e9285be1c3331f46" } diff --git a/api/.sqlx/query-9c87398ff240443d196d307613d955a52919afdb9f695a23ea97eb61975fd10d.json b/api/.sqlx/query-9c87398ff240443d196d307613d955a52919afdb9f695a23ea97eb61975fd10d.json new file mode 100644 index 000000000..2c9c3e94d --- /dev/null +++ b/api/.sqlx/query-9c87398ff240443d196d307613d955a52919afdb9f695a23ea97eb61975fd10d.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM github_identities WHERE github_id = $1\n RETURNING github_id, access_token, access_token_expires_at, refresh_token, refresh_token_expires_at, updated_at, created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "access_token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "access_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "refresh_token", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "refresh_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + true, + true, + true, + false, + false + ] + }, + "hash": "9c87398ff240443d196d307613d955a52919afdb9f695a23ea97eb61975fd10d" +} diff --git a/api/.sqlx/query-b287263be49db488db39d11fd05409c9830a0214be93b8d449a60b3b6bb9828c.json b/api/.sqlx/query-b287263be49db488db39d11fd05409c9830a0214be93b8d449a60b3b6bb9828c.json new file mode 100644 index 000000000..8ed81d781 --- /dev/null +++ b/api/.sqlx/query-b287263be49db488db39d11fd05409c9830a0214be93b8d449a60b3b6bb9828c.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT gitlab_id, access_token, access_token_expires_at, refresh_token, updated_at, created_at\n FROM gitlab_identities\n WHERE gitlab_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "access_token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "access_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "refresh_token", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + true, + true, + false, + false + ] + }, + "hash": "b287263be49db488db39d11fd05409c9830a0214be93b8d449a60b3b6bb9828c" +} diff --git a/api/.sqlx/query-d80fae79967ec1948a779bd947f81e2e3775dca7668bc02941a0db9596caf4d9.json b/api/.sqlx/query-b3120d857e0b35d13bcedcd8484e4bd832ce72065702015257841380050652e6.json similarity index 56% rename from api/.sqlx/query-d80fae79967ec1948a779bd947f81e2e3775dca7668bc02941a0db9596caf4d9.json rename to api/.sqlx/query-b3120d857e0b35d13bcedcd8484e4bd832ce72065702015257841380050652e6.json index 433ef2d4b..009ddfedb 100644 --- a/api/.sqlx/query-d80fae79967ec1948a779bd947f81e2e3775dca7668bc02941a0db9596caf4d9.json +++ b/api/.sqlx/query-b3120d857e0b35d13bcedcd8484e4bd832ce72065702015257841380050652e6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE users SET is_blocked = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "query": "UPDATE users SET is_blocked = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -83,6 +88,7 @@ false, false, true, + true, false, false, false, @@ -91,5 +97,5 @@ null ] }, - "hash": "d80fae79967ec1948a779bd947f81e2e3775dca7668bc02941a0db9596caf4d9" + "hash": "b3120d857e0b35d13bcedcd8484e4bd832ce72065702015257841380050652e6" } diff --git a/api/.sqlx/query-8e86c81c5ef9f753b80d07b457f32ec11f7878bae0665d9e6beaaf8e6ba64be3.json b/api/.sqlx/query-b71c8563a217c5b1a2ffbbdc02cdb3df6a7092150bbccb86da299176e3738100.json similarity index 89% rename from api/.sqlx/query-8e86c81c5ef9f753b80d07b457f32ec11f7878bae0665d9e6beaaf8e6ba64be3.json rename to api/.sqlx/query-b71c8563a217c5b1a2ffbbdc02cdb3df6a7092150bbccb86da299176e3738100.json index 5d9dd6dda..62924b4e7 100644 --- a/api/.sqlx/query-8e86c81c5ef9f753b80d07b457f32ec11f7878bae0665d9e6beaaf8e6ba64be3.json +++ b/api/.sqlx/query-b71c8563a217c5b1a2ffbbdc02cdb3df6a7092150bbccb86da299176e3738100.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH task AS (\n INSERT INTO publishing_tasks (user_id, package_scope, package_name, package_version, config_file)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING\n id,\n status,\n error,\n user_id,\n package_scope,\n package_name,\n package_version,\n config_file,\n created_at,\n updated_at\n )\n SELECT\n task.id as \"task_id\",\n task.status as \"task_status: PublishingTaskStatus\",\n task.error as \"task_error: PublishingTaskError\",\n task.user_id as \"task_user_id\",\n task.package_scope as \"task_package_scope: ScopeName\",\n task.package_name as \"task_package_name: PackageName\",\n task.package_version as \"task_package_version: Version\",\n task.config_file as \"task_config_file: PackagePath\",\n task.created_at as \"task_created_at\",\n task.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM task\n LEFT JOIN users ON task.user_id = users.id", + "query": "WITH task AS (\n INSERT INTO publishing_tasks (user_id, package_scope, package_name, package_version, config_file)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING\n id,\n status,\n error,\n user_id,\n package_scope,\n package_name,\n package_version,\n config_file,\n created_at,\n updated_at\n )\n SELECT\n task.id as \"task_id\",\n task.status as \"task_status: PublishingTaskStatus\",\n task.error as \"task_error: PublishingTaskError\",\n task.user_id as \"task_user_id\",\n task.package_scope as \"task_package_scope: ScopeName\",\n task.package_name as \"task_package_name: PackageName\",\n task.package_version as \"task_package_version: Version\",\n task.config_file as \"task_config_file: PackagePath\",\n task.created_at as \"task_created_at\",\n task.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\nusers.gitlab_id as \"user_gitlab_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM task\n LEFT JOIN users ON task.user_id = users.id", "describe": { "columns": [ { @@ -88,11 +88,16 @@ }, { "ordinal": 14, + "name": "user_gitlab_id?", + "type_info": "Int8" + }, + { + "ordinal": 15, "name": "user_updated_at?", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_created_at?", "type_info": "Timestamptz" } @@ -122,8 +127,9 @@ true, true, true, + true, true ] }, - "hash": "8e86c81c5ef9f753b80d07b457f32ec11f7878bae0665d9e6beaaf8e6ba64be3" + "hash": "b71c8563a217c5b1a2ffbbdc02cdb3df6a7092150bbccb86da299176e3738100" } diff --git a/api/.sqlx/query-47f73f62df1ee26b2c9081d55e53f90c5cac0c79502916bcc8b559474ae64fc7.json b/api/.sqlx/query-b971559b22e6fa42c0589a041b218cb574d5afc11200c629e0a2b55c1b70e183.json similarity index 80% rename from api/.sqlx/query-47f73f62df1ee26b2c9081d55e53f90c5cac0c79502916bcc8b559474ae64fc7.json rename to api/.sqlx/query-b971559b22e6fa42c0589a041b218cb574d5afc11200c629e0a2b55c1b70e183.json index 5fe9892fc..e332394bc 100644 --- a/api/.sqlx/query-47f73f62df1ee26b2c9081d55e53f90c5cac0c79502916bcc8b559474ae64fc7.json +++ b/api/.sqlx/query-b971559b22e6fa42c0589a041b218cb574d5afc11200c629e0a2b55c1b70e183.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n publishing_tasks.id as \"task_id\",\n publishing_tasks.status as \"task_status: PublishingTaskStatus\",\n publishing_tasks.error as \"task_error: PublishingTaskError\",\n publishing_tasks.user_id as \"task_user_id\",\n publishing_tasks.package_scope as \"task_package_scope: ScopeName\",\n publishing_tasks.package_name as \"task_package_name: PackageName\",\n publishing_tasks.package_version as \"task_package_version: Version\",\n publishing_tasks.config_file as \"task_config_file: PackagePath\",\n publishing_tasks.created_at as \"task_created_at\",\n publishing_tasks.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM publishing_tasks\n LEFT JOIN users on publishing_tasks.user_id = users.id\n JOIN packages ON publishing_tasks.package_scope = packages.scope AND publishing_tasks.package_name = packages.name\n WHERE publishing_tasks.package_scope = $1 AND publishing_tasks.package_name = $2 AND publishing_tasks.created_at >= packages.created_at\n ORDER BY publishing_tasks.package_version DESC", + "query": "SELECT\n publishing_tasks.id as \"task_id\",\n publishing_tasks.status as \"task_status: PublishingTaskStatus\",\n publishing_tasks.error as \"task_error: PublishingTaskError\",\n publishing_tasks.user_id as \"task_user_id\",\n publishing_tasks.package_scope as \"task_package_scope: ScopeName\",\n publishing_tasks.package_name as \"task_package_name: PackageName\",\n publishing_tasks.package_version as \"task_package_version: Version\",\n publishing_tasks.config_file as \"task_config_file: PackagePath\",\n publishing_tasks.created_at as \"task_created_at\",\n publishing_tasks.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\nusers.gitlab_id as \"user_gitlab_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM publishing_tasks\n LEFT JOIN users on publishing_tasks.user_id = users.id\n JOIN packages ON publishing_tasks.package_scope = packages.scope AND publishing_tasks.package_name = packages.name\n WHERE publishing_tasks.package_scope = $1 AND publishing_tasks.package_name = $2 AND publishing_tasks.created_at >= packages.created_at\n ORDER BY publishing_tasks.package_version DESC", "describe": { "columns": [ { @@ -88,11 +88,16 @@ }, { "ordinal": 14, + "name": "user_gitlab_id?", + "type_info": "Int8" + }, + { + "ordinal": 15, "name": "user_updated_at?", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_created_at?", "type_info": "Timestamptz" } @@ -118,9 +123,10 @@ false, false, true, + true, false, false ] }, - "hash": "47f73f62df1ee26b2c9081d55e53f90c5cac0c79502916bcc8b559474ae64fc7" + "hash": "b971559b22e6fa42c0589a041b218cb574d5afc11200c629e0a2b55c1b70e183" } diff --git a/api/.sqlx/query-32220872ceb8f8782dcb4a63488c30a8c054f91b595551538b1458375fe927f8.json b/api/.sqlx/query-ba8b36f55757fca9ae87bd9795661f318d397d277cda98715911c7f99c4d6aa8.json similarity index 78% rename from api/.sqlx/query-32220872ceb8f8782dcb4a63488c30a8c054f91b595551538b1458375fe927f8.json rename to api/.sqlx/query-ba8b36f55757fca9ae87bd9795661f318d397d277cda98715911c7f99c4d6aa8.json index 2c1bf5cd9..8afb1467c 100644 --- a/api/.sqlx/query-32220872ceb8f8782dcb4a63488c30a8c054f91b595551538b1458375fe927f8.json +++ b/api/.sqlx/query-ba8b36f55757fca9ae87bd9795661f318d397d277cda98715911c7f99c4d6aa8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n ticket_messages.ticket_id as \"message_ticket_id\",\n ticket_messages.author as \"message_author\",\n ticket_messages.message as \"message_message\",\n ticket_messages.updated_at as \"message_updated_at\",\n ticket_messages.created_at as \"message_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\"\n FROM ticket_messages\n INNER JOIN users ON users.id = ticket_messages.author\n WHERE ticket_messages.ticket_id = $1 ORDER BY ticket_messages.created_at", + "query": "SELECT\n ticket_messages.ticket_id as \"message_ticket_id\",\n ticket_messages.author as \"message_author\",\n ticket_messages.message as \"message_message\",\n ticket_messages.updated_at as \"message_updated_at\",\n ticket_messages.created_at as \"message_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\"\n FROM ticket_messages\n LEFT JOIN users ON users.id = ticket_messages.author\n WHERE ticket_messages.ticket_id = $1 ORDER BY ticket_messages.created_at", "describe": { "columns": [ { @@ -50,11 +50,16 @@ }, { "ordinal": 9, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 10, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "user_created_at", "type_info": "Timestamptz" } @@ -74,9 +79,10 @@ false, false, true, + true, false, false ] }, - "hash": "32220872ceb8f8782dcb4a63488c30a8c054f91b595551538b1458375fe927f8" + "hash": "ba8b36f55757fca9ae87bd9795661f318d397d277cda98715911c7f99c4d6aa8" } diff --git a/api/.sqlx/query-a084a7422202870d53ea0ed98c712968ba138bcbc91639f6bd05297a69edc9bb.json b/api/.sqlx/query-cbdbcf5ccca3d9b5dc159a81940d29d470329e79fa73e66e8b428a86e83c3982.json similarity index 66% rename from api/.sqlx/query-a084a7422202870d53ea0ed98c712968ba138bcbc91639f6bd05297a69edc9bb.json rename to api/.sqlx/query-cbdbcf5ccca3d9b5dc159a81940d29d470329e79fa73e66e8b428a86e83c3982.json index 07e005c5b..6fc527f4d 100644 --- a/api/.sqlx/query-a084a7422202870d53ea0ed98c712968ba138bcbc91639f6bd05297a69edc9bb.json +++ b/api/.sqlx/query-cbdbcf5ccca3d9b5dc159a81940d29d470329e79fa73e66e8b428a86e83c3982.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE users SET is_staff = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )) as \"newer_ticket_messages_count!\"\n ", + "query": "UPDATE users SET is_staff = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )) as \"newer_ticket_messages_count!\"\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -83,6 +88,7 @@ false, false, true, + true, false, false, false, @@ -91,5 +97,5 @@ null ] }, - "hash": "a084a7422202870d53ea0ed98c712968ba138bcbc91639f6bd05297a69edc9bb" + "hash": "cbdbcf5ccca3d9b5dc159a81940d29d470329e79fa73e66e8b428a86e83c3982" } diff --git a/api/.sqlx/query-50b6bc5a8b1faf290853dd720a0294e5fa03b32e9bb6f62dfccb40e2beb8ac05.json b/api/.sqlx/query-d80ac5225bcc4cb8c204005be242908e46b56aeb3e9a818f075296a70c5fd514.json similarity index 71% rename from api/.sqlx/query-50b6bc5a8b1faf290853dd720a0294e5fa03b32e9bb6f62dfccb40e2beb8ac05.json rename to api/.sqlx/query-d80ac5225bcc4cb8c204005be242908e46b56aeb3e9a818f075296a70c5fd514.json index 7628d406d..9275feb84 100644 --- a/api/.sqlx/query-50b6bc5a8b1faf290853dd720a0294e5fa03b32e9bb6f62dfccb40e2beb8ac05.json +++ b/api/.sqlx/query-d80ac5225bcc4cb8c204005be242908e46b56aeb3e9a818f075296a70c5fd514.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT scope_invites.scope as \"scope_invite_scope: ScopeName\", scope_invites.target_user_id as \"scope_invite_target_user_id\", scope_invites.requesting_user_id as \"scope_invite_requesting_user_id\", scope_invites.updated_at as \"scope_invite_updated_at\", scope_invites.created_at as \"scope_invite_created_at\",\n target_user.id as \"target_user_id\", target_user.name as \"target_user_name\", target_user.avatar_url as \"target_user_avatar_url\", target_user.github_id as \"target_user_github_id\", target_user.updated_at as \"target_user_updated_at\", target_user.created_at as \"target_user_created_at\",\n requesting_user.id as \"requesting_user_id\", requesting_user.name as \"requesting_user_name\", requesting_user.avatar_url as \"requesting_user_avatar_url\", requesting_user.github_id as \"requesting_user_github_id\", requesting_user.updated_at as \"requesting_user_updated_at\", requesting_user.created_at as \"requesting_user_created_at\"\n FROM scope_invites\n LEFT JOIN users AS target_user ON scope_invites.target_user_id = target_user.id\n LEFT JOIN users AS requesting_user ON scope_invites.requesting_user_id = requesting_user.id\n WHERE scope = $1", + "query": "SELECT scope_invites.scope as \"scope_invite_scope: ScopeName\", scope_invites.target_user_id as \"scope_invite_target_user_id\", scope_invites.requesting_user_id as \"scope_invite_requesting_user_id\", scope_invites.updated_at as \"scope_invite_updated_at\", scope_invites.created_at as \"scope_invite_created_at\",\n target_user.id as \"target_user_id\", target_user.name as \"target_user_name\", target_user.avatar_url as \"target_user_avatar_url\", target_user.github_id as \"target_user_github_id\", target_user.gitlab_id as \"target_user_gitlab_id\", target_user.updated_at as \"target_user_updated_at\", target_user.created_at as \"target_user_created_at\",\n requesting_user.id as \"requesting_user_id\", requesting_user.name as \"requesting_user_name\", requesting_user.avatar_url as \"requesting_user_avatar_url\", requesting_user.github_id as \"requesting_user_github_id\", requesting_user.gitlab_id as \"requesting_user_gitlab_id\", requesting_user.updated_at as \"requesting_user_updated_at\", requesting_user.created_at as \"requesting_user_created_at\"\n FROM scope_invites\n LEFT JOIN users AS target_user ON scope_invites.target_user_id = target_user.id\n LEFT JOIN users AS requesting_user ON scope_invites.requesting_user_id = requesting_user.id\n WHERE scope = $1", "describe": { "columns": [ { @@ -50,41 +50,51 @@ }, { "ordinal": 9, + "name": "target_user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 10, "name": "target_user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "target_user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 11, + "ordinal": 12, "name": "requesting_user_id", "type_info": "Uuid" }, { - "ordinal": 12, + "ordinal": 13, "name": "requesting_user_name", "type_info": "Text" }, { - "ordinal": 13, + "ordinal": 14, "name": "requesting_user_avatar_url", "type_info": "Text" }, { - "ordinal": 14, + "ordinal": 15, "name": "requesting_user_github_id", "type_info": "Int8" }, { - "ordinal": 15, + "ordinal": 16, + "name": "requesting_user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 17, "name": "requesting_user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 18, "name": "requesting_user_created_at", "type_info": "Timestamptz" } @@ -104,15 +114,17 @@ false, false, true, + true, false, false, false, false, false, true, + true, false, false ] }, - "hash": "50b6bc5a8b1faf290853dd720a0294e5fa03b32e9bb6f62dfccb40e2beb8ac05" + "hash": "d80ac5225bcc4cb8c204005be242908e46b56aeb3e9a818f075296a70c5fd514" } diff --git a/api/.sqlx/query-dac73d5082ee64112435cb9aea35ad1149cf8426862a83fdd2d9e40e801a44d3.json b/api/.sqlx/query-dac73d5082ee64112435cb9aea35ad1149cf8426862a83fdd2d9e40e801a44d3.json new file mode 100644 index 000000000..0e5cd5064 --- /dev/null +++ b/api/.sqlx/query-dac73d5082ee64112435cb9aea35ad1149cf8426862a83fdd2d9e40e801a44d3.json @@ -0,0 +1,105 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO users (name, email, avatar_url, gitlab_id, is_blocked, is_staff)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT(gitlab_id) DO UPDATE\n SET name = $1, email = $2, avatar_url = $3\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "avatar_url", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "is_blocked", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "is_staff", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "scope_limit", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "invite_count!", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "scope_usage!", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "newer_ticket_messages_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Varchar", + "Text", + "Int8", + "Bool", + "Bool" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "dac73d5082ee64112435cb9aea35ad1149cf8426862a83fdd2d9e40e801a44d3" +} diff --git a/api/.sqlx/query-b8061311d580bd61dd78b462c3fe22a7af377396cc744b2e66323303809aa8df.json b/api/.sqlx/query-e071e22885a42e7c6635890cdb8faa433f8d39dd18b5bea72d953a8291bc0e52.json similarity index 68% rename from api/.sqlx/query-b8061311d580bd61dd78b462c3fe22a7af377396cc744b2e66323303809aa8df.json rename to api/.sqlx/query-e071e22885a42e7c6635890cdb8faa433f8d39dd18b5bea72d953a8291bc0e52.json index 4932a9a74..5fdab6ba1 100644 --- a/api/.sqlx/query-b8061311d580bd61dd78b462c3fe22a7af377396cc744b2e66323303809aa8df.json +++ b/api/.sqlx/query-e071e22885a42e7c6635890cdb8faa433f8d39dd18b5bea72d953a8291bc0e52.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH ticket AS (\n INSERT INTO tickets (kind, creator, meta)\n VALUES ($1, $2, $3)\n RETURNING id, kind, creator, meta, closed, updated_at, created_at\n )\n SELECT\n ticket.id as \"ticket_id\",\n ticket.kind as \"ticket_kind: TicketKind\",\n ticket.creator as \"ticket_creator\",\n ticket.meta as \"ticket_meta\",\n ticket.closed as \"ticket_closed\",\n ticket.updated_at as \"ticket_updated_at\",\n ticket.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM ticket\n INNER JOIN users ON users.id = ticket.creator\n ", + "query": "WITH ticket AS (\n INSERT INTO tickets (kind, creator, meta)\n VALUES ($1, $2, $3)\n RETURNING id, kind, creator, meta, closed, updated_at, created_at\n )\n SELECT\n ticket.id as \"ticket_id\",\n ticket.kind as \"ticket_kind: TicketKind\",\n ticket.creator as \"ticket_creator\",\n ticket.meta as \"ticket_meta\",\n ticket.closed as \"ticket_closed\",\n ticket.updated_at as \"ticket_updated_at\",\n ticket.created_at as \"ticket_created_at\",\n users.id as \"user_id\",\n users.name as \"user_name\",\n users.email as \"user_email\",\n users.avatar_url as \"user_avatar_url\",\n users.github_id as \"user_github_id\",\nusers.gitlab_id as \"user_gitlab_id\",\n users.is_blocked as \"user_is_blocked\",\n users.is_staff as \"user_is_staff\",\n users.scope_limit as \"user_scope_limit\",\n users.updated_at as \"user_updated_at\",\n users.created_at as \"user_created_at\",\n (SELECT COUNT(scope_invites.created_at) FROM scope_invites WHERE scope_invites.target_user_id = users.id) as \"user_invite_count!\",\n (SELECT COUNT(scopes.created_at) FROM scopes WHERE scopes.creator = users.id) as \"user_scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"user_newer_ticket_messages_count!\"\n FROM ticket\n INNER JOIN users ON users.id = ticket.creator\n ", "describe": { "columns": [ { @@ -78,41 +78,46 @@ }, { "ordinal": 12, + "name": "user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 13, "name": "user_is_blocked", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 14, "name": "user_is_staff", "type_info": "Bool" }, { - "ordinal": 14, + "ordinal": 15, "name": "user_scope_limit", "type_info": "Int4" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 17, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 17, + "ordinal": 18, "name": "user_invite_count!", "type_info": "Int8" }, { - "ordinal": 18, + "ordinal": 19, "name": "user_scope_usage!", "type_info": "Int8" }, { - "ordinal": 19, + "ordinal": 20, "name": "user_newer_ticket_messages_count!", "type_info": "Int8" } @@ -150,6 +155,7 @@ true, false, true, + true, false, false, false, @@ -160,5 +166,5 @@ null ] }, - "hash": "b8061311d580bd61dd78b462c3fe22a7af377396cc744b2e66323303809aa8df" + "hash": "e071e22885a42e7c6635890cdb8faa433f8d39dd18b5bea72d953a8291bc0e52" } diff --git a/api/.sqlx/query-e2227498f5d18b36c0ab41ce83a071a83e2da6e7044ab64e134f779c66df44e0.json b/api/.sqlx/query-e2227498f5d18b36c0ab41ce83a071a83e2da6e7044ab64e134f779c66df44e0.json new file mode 100644 index 000000000..139f7abb3 --- /dev/null +++ b/api/.sqlx/query-e2227498f5d18b36c0ab41ce83a071a83e2da6e7044ab64e134f779c66df44e0.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM gitlab_identities WHERE gitlab_id = $1\n RETURNING gitlab_id, access_token, access_token_expires_at, refresh_token, updated_at, created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "access_token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "access_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "refresh_token", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + true, + true, + false, + false + ] + }, + "hash": "e2227498f5d18b36c0ab41ce83a071a83e2da6e7044ab64e134f779c66df44e0" +} diff --git a/api/.sqlx/query-c989f79b5e67eddfa93e1087ac8459e26408884f9d10de81c4c27ba7d55678d5.json b/api/.sqlx/query-e403f18362750577c89ad51c62520d3d16ffca22f5a7fc6e9cf8981c2d1dcaa8.json similarity index 69% rename from api/.sqlx/query-c989f79b5e67eddfa93e1087ac8459e26408884f9d10de81c4c27ba7d55678d5.json rename to api/.sqlx/query-e403f18362750577c89ad51c62520d3d16ffca22f5a7fc6e9cf8981c2d1dcaa8.json index 61454de42..f72863aae 100644 --- a/api/.sqlx/query-c989f79b5e67eddfa93e1087ac8459e26408884f9d10de81c4c27ba7d55678d5.json +++ b/api/.sqlx/query-e403f18362750577c89ad51c62520d3d16ffca22f5a7fc6e9cf8981c2d1dcaa8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT scope_invites.scope as \"scope_invite_scope: ScopeName\", scope_invites.target_user_id as \"scope_invite_target_user_id\", scope_invites.requesting_user_id as \"scope_invite_requesting_user_id\", scope_invites.updated_at as \"scope_invite_updated_at\", scope_invites.created_at as \"scope_invite_created_at\",\n target_user.id as \"target_user_id\", target_user.name as \"target_user_name\", target_user.github_id as \"target_user_github_id\", target_user.avatar_url as \"target_user_avatar_url\", target_user.updated_at as \"target_user_updated_at\", target_user.created_at as \"target_user_created_at\",\n requesting_user.id as \"requesting_user_id\", requesting_user.name as \"requesting_user_name\", requesting_user.github_id as \"requesting_user_github_id\", requesting_user.avatar_url as \"requesting_user_avatar_url\", requesting_user.updated_at as \"requesting_user_updated_at\", requesting_user.created_at as \"requesting_user_created_at\"\n FROM scope_invites\n LEFT JOIN users AS target_user ON scope_invites.target_user_id = target_user.id\n LEFT JOIN users AS requesting_user ON scope_invites.requesting_user_id = requesting_user.id\n WHERE target_user_id = $1", + "query": "SELECT scope_invites.scope as \"scope_invite_scope: ScopeName\", scope_invites.target_user_id as \"scope_invite_target_user_id\", scope_invites.requesting_user_id as \"scope_invite_requesting_user_id\", scope_invites.updated_at as \"scope_invite_updated_at\", scope_invites.created_at as \"scope_invite_created_at\",\n target_user.id as \"target_user_id\", target_user.name as \"target_user_name\", target_user.github_id as \"target_user_github_id\", target_user.gitlab_id as \"target_user_gitlab_id\", target_user.avatar_url as \"target_user_avatar_url\", target_user.updated_at as \"target_user_updated_at\", target_user.created_at as \"target_user_created_at\",\n requesting_user.id as \"requesting_user_id\", requesting_user.name as \"requesting_user_name\", requesting_user.github_id as \"requesting_user_github_id\", requesting_user.gitlab_id as \"requesting_user_gitlab_id\", requesting_user.avatar_url as \"requesting_user_avatar_url\", requesting_user.updated_at as \"requesting_user_updated_at\", requesting_user.created_at as \"requesting_user_created_at\"\n FROM scope_invites\n LEFT JOIN users AS target_user ON scope_invites.target_user_id = target_user.id\n LEFT JOIN users AS requesting_user ON scope_invites.requesting_user_id = requesting_user.id\n WHERE target_user_id = $1", "describe": { "columns": [ { @@ -45,46 +45,56 @@ }, { "ordinal": 8, + "name": "target_user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 9, "name": "target_user_avatar_url", "type_info": "Text" }, { - "ordinal": 9, + "ordinal": 10, "name": "target_user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "target_user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 11, + "ordinal": 12, "name": "requesting_user_id", "type_info": "Uuid" }, { - "ordinal": 12, + "ordinal": 13, "name": "requesting_user_name", "type_info": "Text" }, { - "ordinal": 13, + "ordinal": 14, "name": "requesting_user_github_id", "type_info": "Int8" }, { - "ordinal": 14, + "ordinal": 15, + "name": "requesting_user_gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 16, "name": "requesting_user_avatar_url", "type_info": "Text" }, { - "ordinal": 15, + "ordinal": 17, "name": "requesting_user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 16, + "ordinal": 18, "name": "requesting_user_created_at", "type_info": "Timestamptz" } @@ -103,16 +113,18 @@ false, false, true, + true, false, false, false, false, false, true, + true, false, false, false ] }, - "hash": "c989f79b5e67eddfa93e1087ac8459e26408884f9d10de81c4c27ba7d55678d5" + "hash": "e403f18362750577c89ad51c62520d3d16ffca22f5a7fc6e9cf8981c2d1dcaa8" } diff --git a/api/.sqlx/query-646d1b869984186ad9503d5f4db3fad731a521905545466d6cdc1b705137bee2.json b/api/.sqlx/query-e981982720b74fb66954b71a236cb1d4f5940174b650fe6b7b81e648fd1cf995.json similarity index 56% rename from api/.sqlx/query-646d1b869984186ad9503d5f4db3fad731a521905545466d6cdc1b705137bee2.json rename to api/.sqlx/query-e981982720b74fb66954b71a236cb1d4f5940174b650fe6b7b81e648fd1cf995.json index 4a2f11d29..7f7ea2a10 100644 --- a/api/.sqlx/query-646d1b869984186ad9503d5f4db3fad731a521905545466d6cdc1b705137bee2.json +++ b/api/.sqlx/query-e981982720b74fb66954b71a236cb1d4f5940174b650fe6b7b81e648fd1cf995.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE users SET scope_limit = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "query": "UPDATE users SET scope_limit = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -83,6 +88,7 @@ false, false, true, + true, false, false, false, @@ -91,5 +97,5 @@ null ] }, - "hash": "646d1b869984186ad9503d5f4db3fad731a521905545466d6cdc1b705137bee2" + "hash": "e981982720b74fb66954b71a236cb1d4f5940174b650fe6b7b81e648fd1cf995" } diff --git a/api/.sqlx/query-050b47e8c4f5300bdb04ae9f42a1897f68b0462ad93a29ce20bb652bbbb83420.json b/api/.sqlx/query-ee5bd2a5e51e114f22345b005ae01fede3f9f123d1b6ed1e3ffc1e78cadcdd20.json similarity index 54% rename from api/.sqlx/query-050b47e8c4f5300bdb04ae9f42a1897f68b0462ad93a29ce20bb652bbbb83420.json rename to api/.sqlx/query-ee5bd2a5e51e114f22345b005ae01fede3f9f123d1b6ed1e3ffc1e78cadcdd20.json index b9c4edadc..e598f6145 100644 --- a/api/.sqlx/query-050b47e8c4f5300bdb04ae9f42a1897f68b0462ad93a29ce20bb652bbbb83420.json +++ b/api/.sqlx/query-ee5bd2a5e51e114f22345b005ae01fede3f9f123d1b6ed1e3ffc1e78cadcdd20.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO users (name, email, avatar_url, github_id, is_blocked, is_staff)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "query": "INSERT INTO users (name, email, avatar_url, github_id, is_blocked, is_staff)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT(github_id) DO UPDATE\n SET name = $1, email = $2, avatar_url = $3\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -87,6 +92,7 @@ false, false, true, + true, false, false, false, @@ -95,5 +101,5 @@ null ] }, - "hash": "050b47e8c4f5300bdb04ae9f42a1897f68b0462ad93a29ce20bb652bbbb83420" + "hash": "ee5bd2a5e51e114f22345b005ae01fede3f9f123d1b6ed1e3ffc1e78cadcdd20" } diff --git a/api/.sqlx/query-eedb66f9ce1a747414e7e11e9be1cf91f62d8310e12d6b6f2ed0e1e3afb7d2da.json b/api/.sqlx/query-eedb66f9ce1a747414e7e11e9be1cf91f62d8310e12d6b6f2ed0e1e3afb7d2da.json new file mode 100644 index 000000000..5429fe9c9 --- /dev/null +++ b/api/.sqlx/query-eedb66f9ce1a747414e7e11e9be1cf91f62d8310e12d6b6f2ed0e1e3afb7d2da.json @@ -0,0 +1,101 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET github_id = $1 WHERE id = $2\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "avatar_url", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "is_blocked", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "is_staff", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "scope_limit", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "invite_count!", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "scope_usage!", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "newer_ticket_messages_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "eedb66f9ce1a747414e7e11e9be1cf91f62d8310e12d6b6f2ed0e1e3afb7d2da" +} diff --git a/api/.sqlx/query-021c8083cae4c940b95526f2f04fd31ee41aa9c8abe34ab4e170b03f33467750.json b/api/.sqlx/query-f3909c57b7102d7036f482f77b985c121e9d6082d33a7405ef9c770c219815be.json similarity index 86% rename from api/.sqlx/query-021c8083cae4c940b95526f2f04fd31ee41aa9c8abe34ab4e170b03f33467750.json rename to api/.sqlx/query-f3909c57b7102d7036f482f77b985c121e9d6082d33a7405ef9c770c219815be.json index 81013483c..99c74b951 100644 --- a/api/.sqlx/query-021c8083cae4c940b95526f2f04fd31ee41aa9c8abe34ab4e170b03f33467750.json +++ b/api/.sqlx/query-f3909c57b7102d7036f482f77b985c121e9d6082d33a7405ef9c770c219815be.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n publishing_tasks.id as \"task_id\",\n publishing_tasks.status as \"task_status: PublishingTaskStatus\",\n publishing_tasks.error as \"task_error: PublishingTaskError\",\n publishing_tasks.user_id as \"task_user_id\",\n publishing_tasks.package_scope as \"task_package_scope: ScopeName\",\n publishing_tasks.package_name as \"task_package_name: PackageName\",\n publishing_tasks.package_version as \"task_package_version: Version\",\n publishing_tasks.config_file as \"task_config_file: PackagePath\",\n publishing_tasks.created_at as \"task_created_at\",\n publishing_tasks.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM publishing_tasks\n LEFT JOIN users on publishing_tasks.user_id = users.id\n WHERE publishing_tasks.id = $1", + "query": "SELECT\n publishing_tasks.id as \"task_id\",\n publishing_tasks.status as \"task_status: PublishingTaskStatus\",\n publishing_tasks.error as \"task_error: PublishingTaskError\",\n publishing_tasks.user_id as \"task_user_id\",\n publishing_tasks.package_scope as \"task_package_scope: ScopeName\",\n publishing_tasks.package_name as \"task_package_name: PackageName\",\n publishing_tasks.package_version as \"task_package_version: Version\",\n publishing_tasks.config_file as \"task_config_file: PackagePath\",\n publishing_tasks.created_at as \"task_created_at\",\n publishing_tasks.updated_at as \"task_updated_at\",\n users.id as \"user_id?\",\n users.name as \"user_name?\",\n users.avatar_url as \"user_avatar_url?\",\n users.github_id as \"user_github_id?\",\n users.gitlab_id as \"user_gitlab_id?\",\n users.updated_at as \"user_updated_at?\",\n users.created_at as \"user_created_at?\"\n FROM publishing_tasks\n LEFT JOIN users on publishing_tasks.user_id = users.id\n WHERE publishing_tasks.id = $1", "describe": { "columns": [ { @@ -88,11 +88,16 @@ }, { "ordinal": 14, + "name": "user_gitlab_id?", + "type_info": "Int8" + }, + { + "ordinal": 15, "name": "user_updated_at?", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "user_created_at?", "type_info": "Timestamptz" } @@ -117,9 +122,10 @@ false, false, true, + true, false, false ] }, - "hash": "021c8083cae4c940b95526f2f04fd31ee41aa9c8abe34ab4e170b03f33467750" + "hash": "f3909c57b7102d7036f482f77b985c121e9d6082d33a7405ef9c770c219815be" } diff --git a/api/.sqlx/query-0e8e4c876ea49c287f77dc33a1dd6c41dc7080e695249ff4a5a415df05139ece.json b/api/.sqlx/query-fc8e38be7b126160347c79ae92ef6ff30d3f75428ca38d13b7af99d5f00a8f1a.json similarity index 58% rename from api/.sqlx/query-0e8e4c876ea49c287f77dc33a1dd6c41dc7080e695249ff4a5a415df05139ece.json rename to api/.sqlx/query-fc8e38be7b126160347c79ae92ef6ff30d3f75428ca38d13b7af99d5f00a8f1a.json index de37c9f85..aeccba097 100644 --- a/api/.sqlx/query-0e8e4c876ea49c287f77dc33a1dd6c41dc7080e695249ff4a5a415df05139ece.json +++ b/api/.sqlx/query-fc8e38be7b126160347c79ae92ef6ff30d3f75428ca38d13b7af99d5f00a8f1a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "DELETE FROM users\n WHERE id = $1\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", + "query": "DELETE FROM users\n WHERE id = $1\n RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, "name": "is_blocked", "type_info": "Bool" }, { - "ordinal": 8, + "ordinal": 9, "name": "is_staff", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "scope_limit", "type_info": "Int4" }, { - "ordinal": 10, + "ordinal": 11, "name": "invite_count!", "type_info": "Int8" }, { - "ordinal": 11, + "ordinal": 12, "name": "scope_usage!", "type_info": "Int8" }, { - "ordinal": 12, + "ordinal": 13, "name": "newer_ticket_messages_count!", "type_info": "Int8" } @@ -82,6 +87,7 @@ false, false, true, + true, false, false, false, @@ -90,5 +96,5 @@ null ] }, - "hash": "0e8e4c876ea49c287f77dc33a1dd6c41dc7080e695249ff4a5a415df05139ece" + "hash": "fc8e38be7b126160347c79ae92ef6ff30d3f75428ca38d13b7af99d5f00a8f1a" } diff --git a/api/.sqlx/query-feda4a7c215e3c0cab79ddcad6b771df40e620dab1e660a22d52a48f7516abf9.json b/api/.sqlx/query-feda4a7c215e3c0cab79ddcad6b771df40e620dab1e660a22d52a48f7516abf9.json new file mode 100644 index 000000000..c85898f3e --- /dev/null +++ b/api/.sqlx/query-feda4a7c215e3c0cab79ddcad6b771df40e620dab1e660a22d52a48f7516abf9.json @@ -0,0 +1,100 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit,\n (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as \"invite_count!\",\n (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as \"scope_usage!\",\n (CASE WHEN users.is_staff THEN (\n SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = (\n SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id\n )\n )\n ) ELSE (\n SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS (\n SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > (\n SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id\n )\n )\n ) END) as \"newer_ticket_messages_count!\"\n FROM users\n WHERE gitlab_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "avatar_url", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "gitlab_id", + "type_info": "Int8" + }, + { + "ordinal": 8, + "name": "is_blocked", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "is_staff", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "scope_limit", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "invite_count!", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "scope_usage!", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "newer_ticket_messages_count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "feda4a7c215e3c0cab79ddcad6b771df40e620dab1e660a22d52a48f7516abf9" +} diff --git a/api/migrations/20250630231604_gitlab_identities.sql b/api/migrations/20250630231604_gitlab_identities.sql new file mode 100644 index 000000000..43fd8eb9d --- /dev/null +++ b/api/migrations/20250630231604_gitlab_identities.sql @@ -0,0 +1,12 @@ +CREATE TABLE gitlab_identities ( + gitlab_id bigint NOT NULL UNIQUE, + access_token text, + access_token_expires_at timestamp with time zone, + refresh_token text, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); +SELECT manage_updated_at('gitlab_identities'); +ALTER TABLE users ADD COLUMN gitlab_id bigint; +ALTER TABLE users ADD CONSTRAINT users_gitlab_identities_fk FOREIGN KEY (gitlab_id) REFERENCES gitlab_identities(gitlab_id); +ALTER TABLE users ADD CONSTRAINT gitlab_id_unique UNIQUE (gitlab_id); diff --git a/api/src/api/admin.rs b/api/src/api/admin.rs index ba7551526..0931b5d8c 100644 --- a/api/src/api/admin.rs +++ b/api/src/api/admin.rs @@ -2,7 +2,7 @@ use crate::NpmUrl; use crate::RegistryUrl; use crate::buckets::Buckets; -use crate::orama::OramaClient; +use crate::external::orama::OramaClient; use hyper::Body; use hyper::Request; use routerify::Router; diff --git a/api/src/api/errors.rs b/api/src/api/errors.rs index 42ffe452f..600c31256 100644 --- a/api/src/api/errors.rs +++ b/api/src/api/errors.rs @@ -253,6 +253,18 @@ errors!( status: BAD_REQUEST, "The metadata for the ticket is not in a valid format, should be a key-value of strings.", }, + UnknownLoginService { + status: BAD_REQUEST, + "The login service is not known.", + }, + ConnectTakenService { + status: BAD_REQUEST, + "Another user is already connected with this user from the provided service.", + }, + DisconnectLastService { + status: BAD_REQUEST, + "You cannot disconnect the last connected service.", + }, ); pub fn map_unique_violation(err: sqlx::Error, new_err: ApiError) -> ApiError { @@ -366,6 +378,24 @@ impl From for ApiError { } } +impl + From< + oauth2::RequestTokenError< + oauth2::reqwest::Error, + oauth2::basic::BasicRevocationErrorResponse, + >, + > for ApiError +{ + fn from( + error: oauth2::RequestTokenError< + oauth2::reqwest::Error, + oauth2::basic::BasicRevocationErrorResponse, + >, + ) -> ApiError { + anyhow::Error::from(error).into() + } +} + impl From for ApiError { fn from(error: GcsError) -> ApiError { anyhow::Error::from(error).into() diff --git a/api/src/api/package.rs b/api/src/api/package.rs index fb426fd26..fcb14781d 100644 --- a/api/src/api/package.rs +++ b/api/src/api/package.rs @@ -51,8 +51,7 @@ use crate::NpmUrl; use crate::RegistryUrl; use crate::analysis::JsrResolver; use crate::analysis::ModuleParser; -use crate::auth::GithubOauth2Client; -use crate::auth::access_token; +use crate::auth; use crate::buckets::Buckets; use crate::buckets::UploadTaskBody; use crate::db::CreatePackageResult; @@ -66,6 +65,7 @@ use crate::db::User; use crate::docs::DocNodesByUrl; use crate::docs::DocsRequest; use crate::docs::GeneratedDocsOutput; +use crate::external::orama::OramaClient; use crate::gcp; use crate::gcp::CACHE_CONTROL_DO_NOT_CACHE; use crate::gcp::GcsUploadOptions; @@ -77,7 +77,6 @@ use crate::ids::Version; use crate::metadata::PackageMetadata; use crate::metadata::VersionMetadata; use crate::npm::generate_npm_version_manifest; -use crate::orama::OramaClient; use crate::provenance; use crate::publish::publish_task; use crate::tarball::gcs_tarball_path; @@ -378,7 +377,6 @@ pub async fn update_handler(mut req: Request) -> ApiResult { let db: &Database = req.data::().unwrap(); let orama_client = req.data::>().unwrap(); - let github_oauth2_client = req.data::().unwrap(); let (package, repo, meta) = db .get_package(&scope, &package_name) @@ -430,6 +428,8 @@ pub async fn update_handler(mut req: Request) -> ApiResult { Ok(ApiPackage::from((package, None, meta))) } ApiUpdatePackageRequest::GithubRepository(Some(repo)) => { + let github_oauth2_client = + req.data::().unwrap(); update_github_repository( &user.id, sudo, @@ -582,13 +582,13 @@ async fn update_description( } #[allow(clippy::too_many_arguments)] -#[instrument(skip(db, scope, package, req), err, fields(repo.owner = req.owner, repo.name = req.name))] +#[instrument(skip(db, scope, package, github_oauth2_client, req), err, fields(repo.owner = req.owner, repo.name = req.name))] async fn update_github_repository( actor_id: &Uuid, is_sudo: bool, user: &User, db: &Database, - github_oauth2_client: &GithubOauth2Client, + github_oauth2_client: &auth::github::Oauth2Client, scope: ScopeName, package: PackageName, req: ApiUpdatePackageGithubRepositoryRequest, @@ -601,8 +601,9 @@ async fn update_github_repository( let ghid = db.get_github_identity(gh_user_id).await?; let mut new_ghid = ghid.into(); let access_token = - access_token(db, github_oauth2_client, &mut new_ghid).await?; - let github_u2s_client = crate::github::GitHubUserClient::new(access_token); + auth::github::access_token(db, github_oauth2_client, &mut new_ghid).await?; + let github_u2s_client = + crate::external::github::GitHubUserClient::new(access_token); let repo = github_u2s_client .get_repo(&req.owner, &req.name) diff --git a/api/src/api/scope.rs b/api/src/api/scope.rs index 83219729a..14c06adaf 100644 --- a/api/src/api/scope.rs +++ b/api/src/api/scope.rs @@ -21,8 +21,7 @@ use super::errors::ApiError; use super::errors::map_unique_violation; use super::types::*; -use crate::auth::GithubOauth2Client; -use crate::auth::lookup_user_by_github_login; +use crate::auth; use crate::db::*; use crate::util; use crate::util::ApiResult; @@ -247,7 +246,6 @@ async fn invite_member_handler( let invite = decode_json::(&mut req).await?; let db = req.data::().unwrap(); - let github_oauth2_client = req.data::().unwrap(); db.get_scope(&scope).await?.ok_or(ApiError::ScopeNotFound)?; @@ -256,7 +254,9 @@ async fn invite_member_handler( let new_user = match invite { ApiAddScopeMemberRequest::GithubLogin(github_login) => { - lookup_user_by_github_login( + let github_oauth2_client = + req.data::().unwrap(); + auth::github::lookup_user_by_github_login( db, github_oauth2_client, current_user, @@ -265,6 +265,18 @@ async fn invite_member_handler( .await? .ok_or(ApiError::UserNotFound)? } + ApiAddScopeMemberRequest::GitlabUsername(gitlab_username) => { + let gitlab_oauth2_client = + req.data::().unwrap(); + auth::gitlab::lookup_user_by_gitlab_username( + db, + gitlab_oauth2_client, + current_user, + &gitlab_username, + ) + .await? + .ok_or(ApiError::UserNotFound)? + } ApiAddScopeMemberRequest::Id(id) => { db.get_user(id).await?.ok_or(ApiError::UserNotFound)? } diff --git a/api/src/api/types.rs b/api/src/api/types.rs index 0bc776a2b..a7fa255de 100644 --- a/api/src/api/types.rs +++ b/api/src/api/types.rs @@ -120,6 +120,7 @@ pub struct ApiUser { pub id: Uuid, pub name: String, pub github_id: Option, + pub gitlab_id: Option, pub avatar_url: String, pub updated_at: DateTime, pub created_at: DateTime, @@ -131,6 +132,7 @@ impl From for ApiUser { id: user.id, name: user.name, github_id: user.github_id, + gitlab_id: user.gitlab_id, avatar_url: user.avatar_url, updated_at: user.updated_at, created_at: user.created_at, @@ -144,6 +146,7 @@ impl From for ApiUser { id: user.id, name: user.name, github_id: user.github_id, + gitlab_id: user.gitlab_id, avatar_url: user.avatar_url, updated_at: user.updated_at, created_at: user.created_at, @@ -161,6 +164,7 @@ pub struct ApiFullUser { pub updated_at: DateTime, pub created_at: DateTime, pub github_id: Option, + pub gitlab_id: Option, pub is_blocked: bool, pub is_staff: bool, pub scope_usage: i32, @@ -179,6 +183,7 @@ impl From for ApiFullUser { updated_at: user.updated_at, created_at: user.created_at, github_id: user.github_id, + gitlab_id: user.gitlab_id, is_blocked: user.is_blocked, is_staff: user.is_staff, scope_usage: user.scope_usage as i32, @@ -298,6 +303,7 @@ impl From<(ScopeMember, UserPublic)> for ApiScopeMember { #[serde(rename_all = "camelCase")] pub enum ApiAddScopeMemberRequest { GithubLogin(String), + GitlabUsername(String), Id(Uuid), } diff --git a/api/src/auth.rs b/api/src/auth.rs deleted file mode 100644 index 5e0a4b107..000000000 --- a/api/src/auth.rs +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright 2024 the JSR authors. All rights reserved. MIT license. -// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. - -use crate::api::ApiError; -use crate::db::*; -use crate::util::ApiResult; -use crate::util::sanitize_redirect_url; -use chrono::DateTime; -use chrono::Duration; -use chrono::Utc; -use hyper::Body; -use hyper::Request; -use hyper::Response; -use hyper::StatusCode; -use hyper::header; -use oauth2::ExtraTokenFields; -use oauth2::Scope; -use oauth2::StandardRevocableToken; -use oauth2::StandardTokenIntrospectionResponse; -use oauth2::StandardTokenResponse; -use oauth2::TokenResponse; -use oauth2::basic::BasicErrorResponse; -use oauth2::basic::BasicRevocationErrorResponse; -use oauth2::basic::BasicTokenType; -use oauth2::reqwest::async_http_client; -use routerify::ext::RequestExt; -use routerify_query::RequestQueryExt; -use serde::Deserialize; -use serde::Serialize; -use tracing::Span; -use tracing::instrument; - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct GithubTokenExtraFields { - pub refresh_token_expires_in: Option, -} -impl ExtraTokenFields for GithubTokenExtraFields {} - -type GithubTokenResponse = - StandardTokenResponse; - -pub type GithubOauth2Client = oauth2::Client< - BasicErrorResponse, - GithubTokenResponse, - BasicTokenType, - StandardTokenIntrospectionResponse, - StandardRevocableToken, - BasicRevocationErrorResponse, ->; - -fn new_github_identity_from_oauth_response( - res: StandardTokenResponse, -) -> NewGithubIdentity { - let now = Utc::now(); - let access_token = res.access_token().secret().to_string(); - let access_token_expires_in = res.expires_in().unwrap(); - let access_token_expires_at = - now + Duration::from_std(access_token_expires_in).unwrap(); - - let refresh_token = res.refresh_token().map(|t| t.secret().to_string()); - let refresh_token_expires_at = res - .extra_fields() - .refresh_token_expires_in - .map(|s| now + Duration::try_seconds(s).unwrap()); - - NewGithubIdentity { - github_id: 0, - access_token: Some(access_token), - access_token_expires_at: Some(access_token_expires_at), - refresh_token, - refresh_token_expires_at, - } -} - -#[instrument(skip(db, github_oauth2_client, ghid), err, fields(user.github_id = ghid.github_id))] -pub async fn access_token( - db: &Database, - github_oauth2_client: &GithubOauth2Client, - ghid: &mut NewGithubIdentity, -) -> Result { - let now = Utc::now() + Duration::try_seconds(30).unwrap(); - - // If access token is present, and is expired, attempt to refresh it. - if ghid.access_token.is_some() - && ghid.access_token_expires_at.is_some() - && now > ghid.access_token_expires_at.unwrap() - { - if ghid.refresh_token.is_none() || ghid.refresh_token_expires_at.is_none() { - anyhow::bail!( - "Failed to refresh access token, because no refresh token is present.", - ) - } - - // If refresh token is expired, error. - if now > ghid.refresh_token_expires_at.unwrap() { - anyhow::bail!( - "Failed to refresh access token, because refresh token is expired." - ) - } - - // Get new tokens - let res = github_oauth2_client - .exchange_refresh_token(&oauth2::RefreshToken::new( - ghid.refresh_token.clone().unwrap(), - )) - .request_async(async_http_client) - .await?; - let new_github_identity = new_github_identity_from_oauth_response(res); - ghid.access_token = new_github_identity.access_token; - ghid.access_token_expires_at = new_github_identity.access_token_expires_at; - ghid.refresh_token = new_github_identity.refresh_token; - ghid.refresh_token_expires_at = - new_github_identity.refresh_token_expires_at; - - db.upsert_github_identity(ghid.clone()).await?; - } - - match (ghid.access_token.clone(), ghid.access_token_expires_at) { - (Some(access_token), Some(access_token_expires_at)) - if now <= access_token_expires_at => - { - Ok(access_token) - } - _ => Err(anyhow::anyhow!( - "Failed to get access token, because no valid credentials are present.", - )), - } -} - -#[instrument(skip(db, github_oauth2_client, res), err)] -async fn generate_access_token( - db: &Database, - github_oauth2_client: &GithubOauth2Client, - res: GithubTokenResponse, -) -> ApiResult<(String, DateTime)> { - let mut github_identity = new_github_identity_from_oauth_response(res); - - let access_token = - access_token(db, github_oauth2_client, &mut github_identity).await?; - - let gh = crate::github::GitHubUserClient::new(access_token); - let gh_user = gh.current_user().await?; - - github_identity.github_id = gh_user.id; - - db.upsert_github_identity(github_identity).await?; - - let name = gh_user.name.unwrap_or(gh_user.login); - let gh_email = match gh_user.email.as_ref() { - Some(email) => Some(email.clone()), // Email address from public profile. - None => gh - .emails() - .await? - .into_iter() - .filter(|e| e.primary && e.verified) - .map(|e| e.email) - .next(), - }; - - let new_user = NewUser { - name: name.as_str(), - email: gh_email.as_deref(), - avatar_url: gh_user.avatar_url.as_str(), - github_id: Some(gh_user.id), - is_blocked: false, - is_staff: false, - }; - - let db_user = db.upsert_user_by_github_id(new_user).await?; - - let expires_at = Utc::now() + Duration::try_days(7).unwrap(); - - let token_string = crate::token::create_token( - db, - db_user.id, - TokenType::Web, - None, - Some(expires_at), - None, - ) - .await?; - - Ok((token_string, expires_at)) -} - -#[instrument(name = "GET /login", skip(req), err, fields(redirect))] -pub async fn login_handler(req: Request) -> ApiResult> { - let (pkce_code_challenge, pkce_code_verifier) = - oauth2::PkceCodeChallenge::new_random_sha256(); - let github_oauth2_client = req.data::().unwrap(); - let authorization_request = - github_oauth2_client.authorize_url(oauth2::CsrfToken::new_random); - - let (auth_url, csrf_token) = authorization_request - // Set the desired scopes. - .add_scope(Scope::new("read:user".to_string())) - .add_scope(Scope::new("user:email".to_string())) - // Set the PKCE code challenge. - .set_pkce_challenge(pkce_code_challenge) - .url(); - - let mut redirect_url = req - .query("redirect") - .and_then(|url| urlencoding::decode(url).map(|url| url.into_owned()).ok()) - .unwrap_or("/".to_string()); - - redirect_url = sanitize_redirect_url(&redirect_url); - - Span::current().record("redirect", &redirect_url); - - let db = req.data::().unwrap(); - let new_oauth_state = NewOauthState { - csrf_token: csrf_token.secret(), - pkce_code_verifier: pkce_code_verifier.secret(), - redirect_url: &redirect_url, - }; - db.insert_oauth_state(new_oauth_state).await?; - - Ok( - Response::builder() - .status(StatusCode::TEMPORARY_REDIRECT) - .header(header::LOCATION, auth_url.as_str()) - .body(Body::empty()) - .unwrap(), - ) -} - -#[instrument(name = "GET /login/callback", skip(req), err, fields(state))] -pub async fn login_callback_handler( - req: Request, -) -> ApiResult> { - if let Some(err_message) = req.query("error_description") { - return Err(ApiError::GithubOauthError { - msg: err_message.to_owned(), - }); - }; - - let code = req - .query("code") - .ok_or_else(|| ApiError::MalformedRequest { - msg: "missing 'code' query parameter".into(), - })? - .to_owned(); - let state = req - .query("state") - .ok_or_else(|| ApiError::MalformedRequest { - msg: "missing 'state' query parameter".into(), - })?; - let db = req.data::().unwrap(); - - let oauth_state = db - .get_oauth_state(state) - .await? - .ok_or(ApiError::InvalidOauthState)?; - - let github_oauth2_client = req.data::().unwrap(); - let res = github_oauth2_client - .exchange_code(oauth2::AuthorizationCode::new(code)) - .set_pkce_verifier(oauth2::PkceCodeVerifier::new( - oauth_state.pkce_code_verifier, - )) - .request_async(async_http_client) - .await?; - - db.delete_oauth_state(&oauth_state.csrf_token).await?; - - let (token, expires_at) = - generate_access_token(db, github_oauth2_client, res).await?; - - let res = Response::builder() - .status(StatusCode::FOUND) - .header(header::CONTENT_TYPE, "text/html") - .header(header::LOCATION, oauth_state.redirect_url) - .header( - header::SET_COOKIE, - format!( - "token={token}; Expires={}; Path=/; SameSite=Lax; HttpOnly", - expires_at.to_rfc2822().replace("+0000", "GMT"), - ), - ) - .body(Body::empty()) - .unwrap(); - Ok(res) -} - -#[instrument(name = "GET /logout", skip(req), err, fields(redirect))] -pub async fn logout_handler(req: Request) -> ApiResult> { - let mut redirect_url = req - .query("redirect") - .and_then(|url| urlencoding::decode(url).map(|url| url.into_owned()).ok()) - .unwrap_or("/".to_string()); - - redirect_url = sanitize_redirect_url(&redirect_url); - Span::current().record("redirect", &redirect_url); - - Ok( - Response::builder() - .status(StatusCode::SEE_OTHER) - .header( - header::SET_COOKIE, - r#"token=""; Max-Age=0; Path=/; SameSite=Lax; HttpOnly"#, - ) - .header(header::LOCATION, redirect_url) - .body(Body::empty()) - .unwrap(), - ) -} - -#[cfg(not(test))] -#[instrument( - name = "lookup_user_by_github_login", - skip(db, github_oauth2_client, current_user), - err -)] -pub async fn lookup_user_by_github_login( - db: &Database, - github_oauth2_client: &GithubOauth2Client, - current_user: &User, - github_login: &str, -) -> Result, ApiError> { - let current_gh_user_id = current_user.github_id.ok_or_else(|| { - tracing::error!("user is not linked to a GitHub account"); - ApiError::InternalServerError - })?; - let current_github_identity = - db.get_github_identity(current_gh_user_id).await?; - let mut new_ghid = current_github_identity.into(); - let access_token = - access_token(db, github_oauth2_client, &mut new_ghid).await?; - let Some(user) = crate::github::GitHubUserClient::new(access_token) - .get_user(github_login) - .await? - else { - return Ok(None); - }; - Ok(db.get_user_by_github_id(user.id).await?) -} - -#[cfg(test)] -#[instrument( - name = "lookup_user_by_github_login", - skip(db, _github_oauth2_client, _current_user), - err -)] -pub async fn lookup_user_by_github_login( - db: &Database, - _github_oauth2_client: &GithubOauth2Client, - _current_user: &User, - github_login: &str, -) -> Result, ApiError> { - let user = match github_login { - "ry" => db.get_user_by_github_id(101).await?, - "lucacasonato" => db.get_user_by_github_id(102).await?, - "crowlkats" => db.get_user_by_github_id(103).await?, - "bartlomieju" => db.get_user_by_github_id(104).await?, - _ => None, - }; - - Ok(user) -} - -#[cfg(test)] -mod tests { - use crate::api::ApiFullUser; - use hyper::StatusCode; - use serde_json::json; - - //use super::*; - use crate::util::test::{ApiResultExt, TestSetup}; - - #[tokio::test] - async fn user_admin_api() { - let mut t = TestSetup::new().await; - let mock_user_id: uuid::Uuid = - "00000000-0000-0000-0000-000000000000".try_into().unwrap(); - - let user = t.db().get_user(mock_user_id).await.unwrap().unwrap(); - assert!(!user.is_staff); - assert!(!user.is_blocked); - - let token = t.staff_user.token.clone(); - let resp = t - .http() - .patch(format!("/api/admin/users/{}", mock_user_id)) - .body_json(json!({ - "isStaff": true - })) - .token(Some(&token)) - .call() - .await - .unwrap(); - - eprintln!("resp status {}", resp.status()); - assert!(resp.status().is_success()); - let user = t.db().get_user(mock_user_id).await.unwrap().unwrap(); - assert!(user.is_staff); - assert!(!user.is_blocked); - - // Try again without authorization header - let resp = t - .http() - .patch(format!("/api/admin/users/{}", mock_user_id)) - .body_json(json!({ - "isStaff": true - })) - .token(None) - .call() - .await - .unwrap(); - assert_eq!(resp.status(), hyper::StatusCode::UNAUTHORIZED); - - // Turn off admin, turn on blocked, update scope limit - - let resp = t - .http() - .patch(format!("/api/admin/users/{}", mock_user_id)) - .body_json(json!({ - "isStaff": false, - "isBlocked": true, - "scopeLimit": 30, - })) - .token(Some(&token)) - .call() - .await - .unwrap(); - assert!(resp.status().is_success()); - let user = t.db().get_user(mock_user_id).await.unwrap().unwrap(); - assert!(!user.is_staff); - assert!(user.is_blocked); - assert_eq!(user.scope_limit, 30); - } - - #[tokio::test] - async fn blocked() { - let mut t = TestSetup::new().await; - - let path = format!("/api/admin/users/{}", t.user1.user.id); - let token = t.staff_user.token.clone(); - let resp = t - .http() - .patch(path) - .token(Some(&token)) - .body_json(json!({ - "isBlocked": true, - })) - .call() - .await - .unwrap() - .expect_ok::() - .await; - assert!(resp.is_blocked); - - let resp = t - .http() - .get("/api/user") - .call() - .await - .unwrap() - .expect_err(StatusCode::FORBIDDEN) - .await; - assert_eq!(resp.code, "blocked"); - } -} diff --git a/api/src/auth/github.rs b/api/src/auth/github.rs new file mode 100644 index 000000000..bc6de4e70 --- /dev/null +++ b/api/src/auth/github.rs @@ -0,0 +1,272 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. + +use crate::api::ApiError; +use crate::db::*; +use crate::util::ApiResult; +use chrono::DateTime; +use chrono::Duration; +use chrono::Utc; +use oauth2::ExtraTokenFields; +use oauth2::StandardRevocableToken; +use oauth2::StandardTokenIntrospectionResponse; +use oauth2::StandardTokenResponse; +use oauth2::TokenResponse; +use oauth2::basic::BasicErrorResponse; +use oauth2::basic::BasicRevocationErrorResponse; +use oauth2::basic::BasicTokenType; +use oauth2::reqwest::async_http_client; +use serde::Deserialize; +use serde::Serialize; +use tracing::instrument; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct GithubTokenExtraFields { + pub refresh_token_expires_in: Option, +} +impl ExtraTokenFields for GithubTokenExtraFields {} + +type GithubTokenResponse = + StandardTokenResponse; + +#[derive(Clone)] +pub struct Oauth2Client( + pub oauth2::Client< + BasicErrorResponse, + GithubTokenResponse, + BasicTokenType, + StandardTokenIntrospectionResponse, + StandardRevocableToken, + BasicRevocationErrorResponse, + >, + pub String, +); + +impl Oauth2Client { + pub fn new(id: String, secret: String) -> Self { + Self( + oauth2::Client::new( + oauth2::ClientId::new(id), + Some(oauth2::ClientSecret::new(secret.clone())), + oauth2::AuthUrl::new( + "https://github.com/login/oauth/authorize".to_string(), + ) + .unwrap(), + Some( + oauth2::TokenUrl::new( + "https://github.com/login/oauth/access_token".to_string(), + ) + .unwrap(), + ), + ), + secret, + ) + } +} + +fn new_github_identity_from_oauth_response( + res: StandardTokenResponse, +) -> NewGithubIdentity { + let now = Utc::now(); + let access_token = res.access_token().secret().to_string(); + let access_token_expires_in = res.expires_in().unwrap(); + let access_token_expires_at = + now + Duration::from_std(access_token_expires_in).unwrap(); + + let refresh_token = res.refresh_token().map(|t| t.secret().to_string()); + let refresh_token_expires_at = res + .extra_fields() + .refresh_token_expires_in + .map(|s| now + Duration::try_seconds(s).unwrap()); + + NewGithubIdentity { + github_id: 0, + access_token: Some(access_token), + access_token_expires_at: Some(access_token_expires_at), + refresh_token, + refresh_token_expires_at, + } +} + +#[instrument(skip(db, github_oauth2_client, ghid), err, fields(user.github_id = ghid.github_id))] +pub async fn access_token( + db: &Database, + github_oauth2_client: &Oauth2Client, + ghid: &mut NewGithubIdentity, +) -> Result { + let now = Utc::now() + Duration::try_seconds(30).unwrap(); + + // If access token is present, and is expired, attempt to refresh it. + if ghid.access_token.is_some() + && ghid.access_token_expires_at.is_some() + && now > ghid.access_token_expires_at.unwrap() + { + if ghid.refresh_token.is_none() || ghid.refresh_token_expires_at.is_none() { + anyhow::bail!( + "Failed to refresh access token, because no refresh token is present.", + ) + } + + // If refresh token is expired, error. + if now > ghid.refresh_token_expires_at.unwrap() { + anyhow::bail!( + "Failed to refresh access token, because refresh token is expired." + ) + } + + // Get new tokens + let res = github_oauth2_client + .0 + .exchange_refresh_token(&oauth2::RefreshToken::new( + ghid.refresh_token.clone().unwrap(), + )) + .request_async(async_http_client) + .await?; + let new_github_identity = new_github_identity_from_oauth_response(res); + ghid.access_token = new_github_identity.access_token; + ghid.access_token_expires_at = new_github_identity.access_token_expires_at; + ghid.refresh_token = new_github_identity.refresh_token; + ghid.refresh_token_expires_at = + new_github_identity.refresh_token_expires_at; + + db.upsert_github_identity(ghid.clone()).await?; + } + + match (ghid.access_token.clone(), ghid.access_token_expires_at) { + (Some(access_token), Some(access_token_expires_at)) + if now <= access_token_expires_at => + { + Ok(access_token) + } + _ => Err(anyhow::anyhow!( + "Failed to get access token, because no valid credentials are present.", + )), + } +} + +#[instrument(skip(db, github_oauth2_client, res), err)] +pub async fn generate_access_token( + db: &Database, + github_oauth2_client: &Oauth2Client, + res: GithubTokenResponse, + logged_in_user: Option<&User>, +) -> ApiResult<(String, DateTime)> { + let mut github_identity = new_github_identity_from_oauth_response(res); + + let access_token = + access_token(db, github_oauth2_client, &mut github_identity).await?; + + let gh = crate::external::github::GitHubUserClient::new(access_token); + let gh_user = gh.current_user().await?; + + github_identity.github_id = gh_user.id; + + db.upsert_github_identity(github_identity).await?; + + let db_user = if let Some(logged_in_user) = logged_in_user { + if db.get_user_by_github_id(gh_user.id).await?.is_some() { + return Err(ApiError::ConnectTakenService); + } else { + db.user_set_github_id(logged_in_user.id, Some(gh_user.id)) + .await? + } + } else { + let name = gh_user.name.unwrap_or(gh_user.login); + let gh_email = match gh_user.email.as_ref() { + Some(email) => Some(email.clone()), // Email address from public profile. + None => gh + .emails() + .await? + .into_iter() + .filter(|e| e.primary && e.verified) + .map(|e| e.email) + .next(), + }; + + let new_user = NewUser { + name: name.as_str(), + email: gh_email.as_deref(), + avatar_url: gh_user.avatar_url.as_str(), + github_id: Some(gh_user.id), + gitlab_id: None, + is_blocked: false, + is_staff: false, + }; + + db.upsert_user_by_github_id(new_user).await? + }; + + let expires_at = Utc::now() + Duration::try_days(7).unwrap(); + + let token_string = crate::token::create_token( + db, + db_user.id, + TokenType::Web, + None, + Some(expires_at), + None, + ) + .await?; + + Ok((token_string, expires_at)) +} + +#[cfg(not(test))] +#[instrument( + name = "lookup_user_by_github_login", + skip(db, github_oauth2_client, current_user), + err +)] +pub async fn lookup_user_by_github_login( + db: &Database, + github_oauth2_client: &Oauth2Client, + current_user: &User, + github_login: &str, +) -> Result, crate::api::ApiError> { + let current_gh_user_id = current_user.github_id.ok_or_else(|| { + tracing::error!("user is not linked to a GitHub account"); + crate::api::ApiError::InternalServerError + })?; + let current_github_identity = + db.get_github_identity(current_gh_user_id).await?; + let mut new_ghid = current_github_identity.into(); + let access_token = + access_token(db, github_oauth2_client, &mut new_ghid).await?; + let Some(user) = crate::external::github::GitHubUserClient::new(access_token) + .get_user(github_login) + .await? + else { + return Ok(None); + }; + Ok(db.get_user_by_github_id(user.id).await?) +} + +#[cfg(test)] +#[instrument( + name = "lookup_user_by_github_login", + skip(db, _github_oauth2_client, _current_user), + err +)] +pub async fn lookup_user_by_github_login( + db: &Database, + _github_oauth2_client: &Oauth2Client, + _current_user: &User, + github_login: &str, +) -> Result, crate::api::ApiError> { + let user = match github_login { + "ry" => db.get_user_by_github_id(101).await?, + "lucacasonato" => db.get_user_by_github_id(102).await?, + "crowlkats" => db.get_user_by_github_id(103).await?, + "bartlomieju" => db.get_user_by_github_id(104).await?, + _ => None, + }; + + Ok(user) +} + +pub fn set_scopes( + ar: oauth2::AuthorizationRequest, +) -> oauth2::AuthorizationRequest { + ar.add_scope(oauth2::Scope::new("read:user".to_string())) + .add_scope(oauth2::Scope::new("user:email".to_string())) +} diff --git a/api/src/auth/gitlab.rs b/api/src/auth/gitlab.rs new file mode 100644 index 000000000..d92f660dd --- /dev/null +++ b/api/src/auth/gitlab.rs @@ -0,0 +1,255 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. + +use crate::api::ApiError; +use crate::db::*; +use crate::util::ApiResult; +use chrono::DateTime; +use chrono::Duration; +use chrono::Utc; +use oauth2::StandardRevocableToken; +use oauth2::StandardTokenIntrospectionResponse; +use oauth2::StandardTokenResponse; +use oauth2::TokenResponse; +use oauth2::basic::BasicErrorResponse; +use oauth2::basic::BasicRevocationErrorResponse; +use oauth2::basic::BasicTokenType; +use oauth2::reqwest::async_http_client; +use oauth2::{EmptyExtraTokenFields, RedirectUrl, RevocationUrl}; +use tracing::instrument; +use url::Url; + +type GitLabTokenResponse = + StandardTokenResponse; + +#[derive(Clone)] +pub struct Oauth2Client( + pub oauth2::Client< + BasicErrorResponse, + GitLabTokenResponse, + BasicTokenType, + StandardTokenIntrospectionResponse, + StandardRevocableToken, + BasicRevocationErrorResponse, + >, +); + +impl Oauth2Client { + pub fn new(registry_url: &Url, id: String, secret: String) -> Self { + Self( + oauth2::Client::new( + oauth2::ClientId::new(id), + Some(oauth2::ClientSecret::new(secret)), + oauth2::AuthUrl::new("https://gitlab.com/oauth/authorize".to_string()) + .unwrap(), + Some( + oauth2::TokenUrl::new("https://gitlab.com/oauth/token".to_string()) + .unwrap(), + ), + ) + .set_revocation_uri( + RevocationUrl::new("https://gitlab.com/oauth/revoke".to_string()) + .unwrap(), + ) + .set_redirect_uri(RedirectUrl::from_url( + Url::options() + .base_url(Some(registry_url)) + .parse("./login/callback/gitlab") + .unwrap(), + )), + ) + } +} + +fn new_gitlab_identity_from_oauth_response( + res: GitLabTokenResponse, +) -> NewGitlabIdentity { + let now = Utc::now(); + let access_token = res.access_token().secret().to_string(); + let access_token_expires_in = res.expires_in().unwrap(); + let access_token_expires_at = + now + Duration::from_std(access_token_expires_in).unwrap(); + + let refresh_token = res.refresh_token().map(|t| t.secret().to_string()); + + NewGitlabIdentity { + gitlab_id: 0, + access_token: Some(access_token), + access_token_expires_at: Some(access_token_expires_at), + refresh_token, + } +} + +#[instrument(skip(db, gitlab_oauth2_client, glid), err, fields(user.gitlab_id = glid.gitlab_id))] +pub async fn access_token( + db: &Database, + gitlab_oauth2_client: &Oauth2Client, + glid: &mut NewGitlabIdentity, +) -> Result { + let now = Utc::now() + Duration::try_seconds(30).unwrap(); + + // If access token is present, and is expired, attempt to refresh it. + if glid.access_token.is_some() + && glid.access_token_expires_at.is_some() + && now > glid.access_token_expires_at.unwrap() + { + if glid.refresh_token.is_none() { + anyhow::bail!( + "Failed to refresh access token, because no refresh token is present.", + ) + } + + // Get new tokens + let res = gitlab_oauth2_client + .0 + .exchange_refresh_token(&oauth2::RefreshToken::new( + glid.refresh_token.clone().unwrap(), + )) + .request_async(async_http_client) + .await?; + let new_gitlab_identity = new_gitlab_identity_from_oauth_response(res); + glid.access_token = new_gitlab_identity.access_token; + glid.access_token_expires_at = new_gitlab_identity.access_token_expires_at; + glid.refresh_token = new_gitlab_identity.refresh_token; + + db.upsert_gitlab_identity(glid.clone()).await?; + } + + match (glid.access_token.clone(), glid.access_token_expires_at) { + (Some(access_token), Some(access_token_expires_at)) + if now <= access_token_expires_at => + { + Ok(access_token) + } + _ => Err(anyhow::anyhow!( + "Failed to get access token, because no valid credentials are present.", + )), + } +} + +#[instrument(skip(db, gitlab_oauth2_client, res), err)] +pub async fn generate_access_token( + db: &Database, + gitlab_oauth2_client: &Oauth2Client, + res: GitLabTokenResponse, + logged_in_user: Option<&User>, +) -> ApiResult<(String, DateTime)> { + let mut gitlab_identity = new_gitlab_identity_from_oauth_response(res); + + let access_token = + access_token(db, gitlab_oauth2_client, &mut gitlab_identity).await?; + + let gl = crate::external::gitlab::GitLabUserClient::new(access_token); + let gl_user = gl.current_user().await?; + + gitlab_identity.gitlab_id = gl_user.id; + + db.upsert_gitlab_identity(gitlab_identity).await?; + + let db_user = if let Some(logged_in_user) = logged_in_user { + if db.get_user_by_gitlab_id(gl_user.id).await?.is_some() { + return Err(ApiError::ConnectTakenService); + } else { + db.user_set_gitlab_id(logged_in_user.id, Some(gl_user.id)) + .await? + } + } else { + let name = gl_user.name; + let gh_email = match gl_user.email.as_ref() { + Some(email) => Some(email.clone()), // Email address from public profile. + None => gl + .emails() + .await? + .into_iter() + .filter(|e| e.confirmed_at.is_some()) + .map(|e| e.email) + .next(), + }; + + let new_user = NewUser { + name: name.as_str(), + email: gh_email.as_deref(), + avatar_url: gl_user.avatar_url.as_str(), + github_id: None, + gitlab_id: Some(gl_user.id), + is_blocked: false, + is_staff: false, + }; + + db.upsert_user_by_gitlab_id(new_user).await? + }; + + let expires_at = Utc::now() + Duration::try_days(7).unwrap(); + + let token_string = crate::token::create_token( + db, + db_user.id, + TokenType::Web, + None, + Some(expires_at), + None, + ) + .await?; + + Ok((token_string, expires_at)) +} + +#[cfg(not(test))] +#[instrument( + name = "lookup_user_by_gitlab_login", + skip(db, gitlab_oauth2_client, current_user), + err +)] +pub async fn lookup_user_by_gitlab_username( + db: &Database, + gitlab_oauth2_client: &Oauth2Client, + current_user: &User, + gitlab_username: &str, +) -> Result, crate::api::ApiError> { + let current_gl_user_id = current_user.gitlab_id.ok_or_else(|| { + tracing::error!("user is not linked to a GitLab account"); + crate::api::ApiError::InternalServerError + })?; + let current_gitlab_identity = + db.get_gitlab_identity(current_gl_user_id).await?; + let mut new_ghid = current_gitlab_identity.into(); + let access_token = + access_token(db, gitlab_oauth2_client, &mut new_ghid).await?; + let Some(user) = crate::external::gitlab::GitLabUserClient::new(access_token) + .get_user(gitlab_username) + .await? + else { + return Ok(None); + }; + Ok(db.get_user_by_gitlab_id(user.id).await?) +} + +#[cfg(test)] +#[instrument( + name = "lookup_user_by_gitlab_login", + skip(db, _gitlab_oauth2_client, _current_user), + err +)] +pub async fn lookup_user_by_gitlab_username( + db: &Database, + _gitlab_oauth2_client: &Oauth2Client, + _current_user: &User, + gitlab_username: &str, +) -> Result, crate::api::ApiError> { + let user = match gitlab_username { + "ry" => db.get_user_by_gitlab_id(101).await?, + "lucacasonato" => db.get_user_by_gitlab_id(102).await?, + "crowlkats" => db.get_user_by_gitlab_id(103).await?, + "bartlomieju" => db.get_user_by_gitlab_id(104).await?, + _ => None, + }; + + Ok(user) +} + +pub fn set_scopes( + ar: oauth2::AuthorizationRequest, +) -> oauth2::AuthorizationRequest { + ar.add_scope(oauth2::Scope::new("read_user".to_string())) + .add_scope(oauth2::Scope::new("email".to_string())) +} diff --git a/api/src/auth/mod.rs b/api/src/auth/mod.rs new file mode 100644 index 000000000..40334ff04 --- /dev/null +++ b/api/src/auth/mod.rs @@ -0,0 +1,537 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. + +use crate::RegistryUrl; +use crate::api::ApiError; +use crate::db::*; +use crate::iam::ReqIamExt; +use crate::util::ApiResult; +use crate::util::sanitize_redirect_url; +use hyper::Body; +use hyper::Request; +use hyper::Response; +use hyper::StatusCode; +use hyper::header; +use oauth2::reqwest::async_http_client; +use oauth2::{AccessToken, RedirectUrl, RefreshToken, StandardRevocableToken}; +use routerify::ext::RequestExt; +use routerify_query::RequestQueryExt; +use std::borrow::Cow; +use tracing::Span; +use tracing::instrument; +use url::Url; + +pub mod github; +pub mod gitlab; + +enum OauthService { + GitHub, + GitLab, +} + +fn service_param(req: &Request) -> Result { + let service = crate::util::param(req, "service")?; + + Ok(match service.as_str() { + "github" => OauthService::GitHub, + "gitlab" => OauthService::GitLab, + _ => return Err(ApiError::UnknownLoginService), + }) +} + +#[instrument(name = "GET /login/:service", skip(req), err, fields(redirect))] +pub async fn login_handler(req: Request) -> ApiResult> { + let service = service_param(&req)?; + + let (pkce_code_challenge, pkce_code_verifier) = + oauth2::PkceCodeChallenge::new_random_sha256(); + + let (auth_url, csrf_token) = (match &service { + OauthService::GitHub => { + let github_oauth2_client = req.data::().unwrap(); + let authorization_request = github_oauth2_client + .0 + .authorize_url(oauth2::CsrfToken::new_random); + github::set_scopes(authorization_request) + } + OauthService::GitLab => { + let gitlab_oauth2_client = req.data::().unwrap(); + let authorization_request = gitlab_oauth2_client + .0 + .authorize_url(oauth2::CsrfToken::new_random); + gitlab::set_scopes(authorization_request) + } + }) + .set_pkce_challenge(pkce_code_challenge) + .url(); + + let mut redirect_url = req + .query("redirect") + .and_then(|url| urlencoding::decode(url).map(|url| url.into_owned()).ok()) + .unwrap_or("/".to_string()); + + redirect_url = sanitize_redirect_url(&redirect_url); + + Span::current().record("redirect", &redirect_url); + + let db = req.data::().unwrap(); + let new_oauth_state = NewOauthState { + csrf_token: csrf_token.secret(), + pkce_code_verifier: pkce_code_verifier.secret(), + redirect_url: &redirect_url, + }; + db.insert_oauth_state(new_oauth_state).await?; + + Ok( + Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header(header::LOCATION, auth_url.as_str()) + .body(Body::empty()) + .unwrap(), + ) +} + +#[instrument( + name = "GET /login/callback/:service", + skip(req), + err, + fields(state) +)] +pub async fn login_callback_handler( + req: Request, +) -> ApiResult> { + let service = service_param(&req)?; + + let code = req + .query("code") + .ok_or_else(|| ApiError::MalformedRequest { + msg: "missing 'code' query parameter".into(), + })? + .to_owned(); + let state = req + .query("state") + .ok_or_else(|| ApiError::MalformedRequest { + msg: "missing 'state' query parameter".into(), + })?; + let db = req.data::().unwrap(); + + let oauth_state = db + .get_oauth_state(state) + .await? + .ok_or(ApiError::InvalidOauthState)?; + + let (token, expires_at) = match service { + OauthService::GitHub => { + if let Some(err_message) = req.query("error_description") { + return Err(ApiError::GithubOauthError { + msg: err_message.to_owned(), + }); + }; + + let github_oauth2_client = req.data::().unwrap(); + let res = github_oauth2_client + .0 + .exchange_code(oauth2::AuthorizationCode::new(code)) + .set_pkce_verifier(oauth2::PkceCodeVerifier::new( + oauth_state.pkce_code_verifier, + )) + .request_async(async_http_client) + .await?; + + db.delete_oauth_state(&oauth_state.csrf_token).await?; + + github::generate_access_token(db, github_oauth2_client, res, None).await? + } + OauthService::GitLab => { + let gitlab_oauth2_client = req.data::().unwrap(); + let res = gitlab_oauth2_client + .0 + .exchange_code(oauth2::AuthorizationCode::new(code)) + .set_pkce_verifier(oauth2::PkceCodeVerifier::new( + oauth_state.pkce_code_verifier, + )) + .request_async(async_http_client) + .await?; + + db.delete_oauth_state(&oauth_state.csrf_token).await?; + + gitlab::generate_access_token(db, gitlab_oauth2_client, res, None).await? + } + }; + + let res = Response::builder() + .status(StatusCode::FOUND) + .header(header::CONTENT_TYPE, "text/html") + .header(header::LOCATION, oauth_state.redirect_url) + .header( + header::SET_COOKIE, + format!( + "token={token}; Expires={}; Path=/; SameSite=Lax; HttpOnly", + expires_at.to_rfc2822().replace("+0000", "GMT"), + ), + ) + .body(Body::empty()) + .unwrap(); + Ok(res) +} + +#[instrument(name = "GET /logout", skip(req), err, fields(redirect))] +pub async fn logout_handler(req: Request) -> ApiResult> { + let mut redirect_url = req + .query("redirect") + .and_then(|url| urlencoding::decode(url).map(|url| url.into_owned()).ok()) + .unwrap_or("/".to_string()); + + redirect_url = sanitize_redirect_url(&redirect_url); + Span::current().record("redirect", &redirect_url); + + Ok( + Response::builder() + .status(StatusCode::SEE_OTHER) + .header( + header::SET_COOKIE, + r#"token=""; Max-Age=0; Path=/; SameSite=Lax; HttpOnly"#, + ) + .header(header::LOCATION, redirect_url) + .body(Body::empty()) + .unwrap(), + ) +} + +#[instrument(name = "GET /connect/:service", skip(req), err, fields(redirect))] +pub async fn connect_handler(req: Request) -> ApiResult> { + let service = service_param(&req)?; + + let (pkce_code_challenge, pkce_code_verifier) = + oauth2::PkceCodeChallenge::new_random_sha256(); + + let registry_url = req.data::().unwrap().0.clone(); + + let (auth_url, csrf_token) = (match &service { + OauthService::GitHub => { + let github_oauth2_client = req.data::().unwrap(); + let authorization_request = github_oauth2_client + .0 + .authorize_url(oauth2::CsrfToken::new_random); + github::set_scopes(authorization_request).set_redirect_uri(Cow::Owned( + RedirectUrl::from_url( + Url::options() + .base_url(Some(®istry_url)) + .parse("./connect/callback/github") + .unwrap(), + ), + )) + } + OauthService::GitLab => { + let gitlab_oauth2_client = req.data::().unwrap(); + let authorization_request = gitlab_oauth2_client + .0 + .authorize_url(oauth2::CsrfToken::new_random); + gitlab::set_scopes(authorization_request).set_redirect_uri(Cow::Owned( + RedirectUrl::from_url( + Url::options() + .base_url(Some(®istry_url)) + .parse("./connect/callback/gitlab") + .unwrap(), + ), + )) + } + }) + .set_pkce_challenge(pkce_code_challenge) + .url(); + + let mut redirect_url = req + .query("redirect") + .and_then(|url| urlencoding::decode(url).map(|url| url.into_owned()).ok()) + .unwrap_or("/account/settings".to_string()); + + redirect_url = sanitize_redirect_url(&redirect_url); + + Span::current().record("redirect", &redirect_url); + + let db = req.data::().unwrap(); + let new_oauth_state = NewOauthState { + csrf_token: csrf_token.secret(), + pkce_code_verifier: pkce_code_verifier.secret(), + redirect_url: &redirect_url, + }; + db.insert_oauth_state(new_oauth_state).await?; + + Ok( + Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header(header::LOCATION, auth_url.as_str()) + .body(Body::empty()) + .unwrap(), + ) +} + +#[instrument( + name = "GET /connect/callback/:service", + skip(req), + err, + fields(state) +)] +pub async fn connect_callback_handler( + req: Request, +) -> ApiResult> { + let service = service_param(&req)?; + + let iam = req.iam(); + let user = iam.check_current_user_access()?; + + let code = req + .query("code") + .ok_or_else(|| ApiError::MalformedRequest { + msg: "missing 'code' query parameter".into(), + })? + .to_owned(); + let state = req + .query("state") + .ok_or_else(|| ApiError::MalformedRequest { + msg: "missing 'state' query parameter".into(), + })?; + let db = req.data::().unwrap(); + let registry_url = req.data::().unwrap().0.clone(); + + let oauth_state = db + .get_oauth_state(state) + .await? + .ok_or(ApiError::InvalidOauthState)?; + + match service { + OauthService::GitHub => { + if let Some(err_message) = req.query("error_description") { + return Err(ApiError::GithubOauthError { + msg: err_message.to_owned(), + }); + }; + + let github_oauth2_client = req.data::().unwrap(); + let res = github_oauth2_client + .0 + .exchange_code(oauth2::AuthorizationCode::new(code)) + .set_pkce_verifier(oauth2::PkceCodeVerifier::new( + oauth_state.pkce_code_verifier, + )) + .set_redirect_uri(Cow::Owned(RedirectUrl::from_url( + Url::options() + .base_url(Some(®istry_url)) + .parse("./connect/callback/github") + .unwrap(), + ))) + .request_async(async_http_client) + .await?; + + db.delete_oauth_state(&oauth_state.csrf_token).await?; + + github::generate_access_token(db, github_oauth2_client, res, Some(user)) + .await?; + } + OauthService::GitLab => { + let gitlab_oauth2_client = req.data::().unwrap(); + let res = gitlab_oauth2_client + .0 + .exchange_code(oauth2::AuthorizationCode::new(code)) + .set_pkce_verifier(oauth2::PkceCodeVerifier::new( + oauth_state.pkce_code_verifier, + )) + .set_redirect_uri(Cow::Owned(RedirectUrl::from_url( + Url::options() + .base_url(Some(®istry_url)) + .parse("./connect/callback/gitlab") + .unwrap(), + ))) + .request_async(async_http_client) + .await?; + + db.delete_oauth_state(&oauth_state.csrf_token).await?; + + gitlab::generate_access_token(db, gitlab_oauth2_client, res, Some(user)) + .await?; + } + } + + let res = Response::builder() + .status(StatusCode::FOUND) + .header(header::CONTENT_TYPE, "text/html") + .header(header::LOCATION, oauth_state.redirect_url) + .body(Body::empty()) + .unwrap(); + Ok(res) +} + +#[instrument(name = "GET /disconnect/:service", skip(req), err, fields(state))] +pub async fn disconnect_handler( + req: Request, +) -> ApiResult> { + let service = service_param(&req)?; + + let iam = req.iam(); + let user = iam.check_current_user_access()?; + + let db = req.data::().unwrap(); + + let ids = [user.github_id.is_some(), user.gitlab_id.is_some()]; + if ids.into_iter().filter(|id| *id).count() == 1 { + return Err(ApiError::DisconnectLastService); + } + + match service { + OauthService::GitHub => { + if let Some(github_id) = user.github_id { + db.user_set_github_id(user.id, None).await?; + let identity = db.delete_github_identity(github_id).await?; + let github_oauth2_client = req.data::().unwrap(); + let github_app_client = crate::external::github::GitHubAppClient::new( + github_oauth2_client.0.client_id().as_str().to_string(), + github_oauth2_client.1.clone(), + ); + + github_app_client + .delete_authorization(identity.access_token.unwrap()) + .await?; + } + } + OauthService::GitLab => { + if let Some(gitlab_id) = user.gitlab_id { + db.user_set_gitlab_id(user.id, None).await?; + let identity = db.delete_gitlab_identity(gitlab_id).await?; + let gitlab_oauth2_client = req.data::().unwrap(); + gitlab_oauth2_client + .0 + .revoke_token(StandardRevocableToken::RefreshToken( + RefreshToken::new(identity.refresh_token.unwrap()), + ))? + .request_async(async_http_client) + .await?; + gitlab_oauth2_client + .0 + .revoke_token(StandardRevocableToken::AccessToken(AccessToken::new( + identity.access_token.unwrap(), + )))? + .request_async(async_http_client) + .await?; + } + } + } + + let mut redirect_url = req + .query("redirect") + .and_then(|url| urlencoding::decode(url).map(|url| url.into_owned()).ok()) + .unwrap_or("/account/settings".to_string()); + + redirect_url = sanitize_redirect_url(&redirect_url); + Span::current().record("redirect", &redirect_url); + + Ok( + Response::builder() + .status(StatusCode::SEE_OTHER) + .header(header::LOCATION, redirect_url) + .body(Body::empty()) + .unwrap(), + ) +} + +#[cfg(test)] +mod tests { + use crate::api::ApiFullUser; + use hyper::StatusCode; + use serde_json::json; + + //use super::*; + use crate::util::test::{ApiResultExt, TestSetup}; + + #[tokio::test] + async fn user_admin_api() { + let mut t = TestSetup::new().await; + let mock_user_id: uuid::Uuid = + "00000000-0000-0000-0000-000000000000".try_into().unwrap(); + + let user = t.db().get_user(mock_user_id).await.unwrap().unwrap(); + assert!(!user.is_staff); + assert!(!user.is_blocked); + + let token = t.staff_user.token.clone(); + let resp = t + .http() + .patch(format!("/api/admin/users/{}", mock_user_id)) + .body_json(json!({ + "isStaff": true + })) + .token(Some(&token)) + .call() + .await + .unwrap(); + + eprintln!("resp status {}", resp.status()); + assert!(resp.status().is_success()); + let user = t.db().get_user(mock_user_id).await.unwrap().unwrap(); + assert!(user.is_staff); + assert!(!user.is_blocked); + + // Try again without authorization header + let resp = t + .http() + .patch(format!("/api/admin/users/{}", mock_user_id)) + .body_json(json!({ + "isStaff": true + })) + .token(None) + .call() + .await + .unwrap(); + assert_eq!(resp.status(), hyper::StatusCode::UNAUTHORIZED); + + // Turn off admin, turn on blocked, update scope limit + + let resp = t + .http() + .patch(format!("/api/admin/users/{}", mock_user_id)) + .body_json(json!({ + "isStaff": false, + "isBlocked": true, + "scopeLimit": 30, + })) + .token(Some(&token)) + .call() + .await + .unwrap(); + assert!(resp.status().is_success()); + let user = t.db().get_user(mock_user_id).await.unwrap().unwrap(); + assert!(!user.is_staff); + assert!(user.is_blocked); + assert_eq!(user.scope_limit, 30); + } + + #[tokio::test] + async fn blocked() { + let mut t = TestSetup::new().await; + + let path = format!("/api/admin/users/{}", t.user1.user.id); + let token = t.staff_user.token.clone(); + let resp = t + .http() + .patch(path) + .token(Some(&token)) + .body_json(json!({ + "isBlocked": true, + })) + .call() + .await + .unwrap() + .expect_ok::() + .await; + assert!(resp.is_blocked); + + let resp = t + .http() + .get("/api/user") + .call() + .await + .unwrap() + .expect_err(StatusCode::FORBIDDEN) + .await; + assert_eq!(resp.code, "blocked"); + } +} diff --git a/api/src/config.rs b/api/src/config.rs index 0abdec7fb..06eed68b0 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -65,6 +65,14 @@ pub struct Config { /// The GitHub Client Secret pub github_client_secret: String, + #[clap(long = "gitlab_client_id", env = "GITLAB_CLIENT_ID")] + /// The GitLab Client ID + pub gitlab_client_id: String, + + #[clap(long = "gitlab_client_secret", env = "GITLAB_CLIENT_SECRET")] + /// The GitLab Client Secret + pub gitlab_client_secret: String, + #[clap(long = "orama_package_index_id", env = "ORAMA_PACKAGE_INDEX_ID")] /// The Orama index for package search pub orama_package_index_id: Option, diff --git a/api/src/db/database.rs b/api/src/db/database.rs index 24fde5ee7..fe45abf67 100644 --- a/api/src/db/database.rs +++ b/api/src/db/database.rs @@ -78,7 +78,7 @@ impl Database { pub async fn get_user(&self, id: Uuid) -> Result> { sqlx::query_as!( User, - r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -106,7 +106,7 @@ impl Database { pub async fn get_user_public(&self, id: Uuid) -> Result> { sqlx::query_as!( UserPublic, - r#"SELECT id, name, avatar_url, github_id, updated_at, created_at + r#"SELECT id, name, avatar_url, github_id, gitlab_id, updated_at, created_at FROM users WHERE id = $1"#, id @@ -122,7 +122,7 @@ impl Database { ) -> Result> { sqlx::query_as!( User, - r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -146,6 +146,37 @@ impl Database { .await } + #[instrument(name = "Database::get_user_by_gitlab_id", skip(self), err)] + pub async fn get_user_by_gitlab_id( + &self, + gitlab_id: i64, + ) -> Result> { + sqlx::query_as!( + User, + r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, + (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", + (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", + (CASE WHEN users.is_staff THEN ( + SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = ( + SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id + ) + ) + ) ELSE ( + SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > ( + SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id + ) + ) + ) END) as "newer_ticket_messages_count!" + FROM users + WHERE gitlab_id = $1"#, + gitlab_id + ) + .fetch_optional(&self.pool) + .await + } + #[instrument(name = "Database::list_users", skip(self), err)] pub async fn list_users( &self, @@ -178,7 +209,7 @@ impl Database { } || "created_at DESC"); let users: Vec = sqlx::query_as( - &format!(r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + &format!(r#"SELECT id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage", (CASE WHEN users.is_staff THEN ( @@ -228,9 +259,9 @@ impl Database { pub async fn insert_user(&self, new_user: NewUser<'_>) -> Result { sqlx::query_as!( User, - r#"INSERT INTO users (name, email, avatar_url, github_id, is_blocked, is_staff) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + r#"INSERT INTO users (name, email, avatar_url, github_id, gitlab_id, is_blocked, is_staff) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -251,6 +282,7 @@ impl Database { new_user.email, new_user.avatar_url, new_user.github_id, + new_user.gitlab_id, new_user.is_blocked, new_user.is_staff ) @@ -274,7 +306,7 @@ impl Database { VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT(github_id) DO UPDATE SET name = $1, email = $2, avatar_url = $3 - RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -302,6 +334,116 @@ impl Database { .await } + #[instrument(name = "Database::upsert_user_by_gitlab_id", skip( + self, + new_user + ), err, fields(user.name = new_user.name, user.email = new_user.email, user.avatar_url = new_user.avatar_url, user.github_id = new_user.github_id, user.gitlab_id = new_user.gitlab_id, user.is_blocked = new_user.is_blocked, user.is_staff = new_user.is_staff + ))] + pub async fn upsert_user_by_gitlab_id( + &self, + new_user: NewUser<'_>, + ) -> Result { + assert!(new_user.gitlab_id.is_some(), "gitlab_id is required"); + sqlx::query_as!( + User, + r#"INSERT INTO users (name, email, avatar_url, gitlab_id, is_blocked, is_staff) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT(gitlab_id) DO UPDATE + SET name = $1, email = $2, avatar_url = $3 + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, + (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", + (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", + (CASE WHEN users.is_staff THEN ( + SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = ( + SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id + ) + ) + ) ELSE ( + SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > ( + SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id + ) + ) + ) END) as "newer_ticket_messages_count!" + "#, + new_user.name, + new_user.email, + new_user.avatar_url, + new_user.gitlab_id, + new_user.is_blocked, + new_user.is_staff + ) + .fetch_one(&self.pool) + .await + } + + #[instrument(name = "Database::user_set_github_id", skip(self), err)] + pub async fn user_set_github_id( + &self, + user: Uuid, + id: Option, + ) -> Result { + sqlx::query_as!( + User, + r#"UPDATE users SET github_id = $1 WHERE id = $2 + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, + (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", + (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", + (CASE WHEN users.is_staff THEN ( + SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = ( + SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id + ) + ) + ) ELSE ( + SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > ( + SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id + ) + ) + ) END) as "newer_ticket_messages_count!" + "#, + id, + user as _, + ) + .fetch_one(&self.pool) + .await + } + + #[instrument(name = "Database::user_set_gitlab_id", skip(self), err)] + pub async fn user_set_gitlab_id( + &self, + user: Uuid, + id: Option, + ) -> Result { + sqlx::query_as!( + User, + r#"UPDATE users SET gitlab_id = $1 WHERE id = $2 + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, + (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", + (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", + (CASE WHEN users.is_staff THEN ( + SELECT count(tickets.created_at) FROM tickets WHERE closed = false AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author = tickets.creator AND tm.created_at = ( + SELECT MAX(ticket_messages.created_at) FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id + ) + ) + ) ELSE ( + SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS ( + SELECT 1 FROM ticket_messages as tm WHERE tm.ticket_id = tickets.id AND tm.author != users.id AND tm.created_at > ( + SELECT MAX(tm2.created_at) FROM ticket_messages as tm2 WHERE tm2.ticket_id = tm.ticket_id AND tm2.author = users.id + ) + ) + ) END) as "newer_ticket_messages_count!" + "#, + id, + user as _, + ) + .fetch_one(&self.pool) + .await + } + #[instrument(name = "Database::user_set_staff", skip(self), err)] pub async fn user_set_staff( &self, @@ -326,7 +468,7 @@ impl Database { let user = sqlx::query_as!( User, r#"UPDATE users SET is_staff = $1 WHERE id = $2 - RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (SELECT COUNT(created_at) FROM tickets WHERE closed = false AND tickets.creator = users.id AND EXISTS ( @@ -370,7 +512,7 @@ impl Database { let user = sqlx::query_as!( User, r#"UPDATE users SET is_blocked = $1 WHERE id = $2 - RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -422,7 +564,7 @@ impl Database { let user = sqlx::query_as!( User, r#"UPDATE users SET scope_limit = $1 WHERE id = $2 - RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -456,7 +598,7 @@ impl Database { User, r#"DELETE FROM users WHERE id = $1 - RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, is_blocked, is_staff, scope_limit, + RETURNING id, name, email, avatar_url, updated_at, created_at, github_id, gitlab_id, is_blocked, is_staff, scope_limit, (SELECT COUNT(created_at) FROM scope_invites WHERE target_user_id = id) as "invite_count!", (SELECT COUNT(created_at) FROM scopes WHERE creator = id) as "scope_usage!", (CASE WHEN users.is_staff THEN ( @@ -1136,7 +1278,8 @@ impl Database { scopes.require_publishing_from_ci as "scope_require_publishing_from_ci", scopes.updated_at as "scope_updated_at", scopes.created_at as "scope_created_at", - users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at", + users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at", usage.package as "usage_package", usage.new_package_per_week as "usage_new_package_per_week", usage.publish_attempts_per_week as "usage_publish_attempts_per_week" FROM scopes LEFT JOIN users ON scopes.creator = users.id @@ -1168,6 +1311,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -1215,7 +1359,7 @@ impl Database { scopes.verify_oidc_actor as "scope_verify_oidc_actor", scopes.require_publishing_from_ci as "scope_require_publishing_from_ci", scopes.created_at as "scope_created_at", - users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at", + users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at", (SELECT COUNT(created_at) FROM packages WHERE packages.scope = scopes.scope) AS "usage_package", (SELECT COUNT(created_at) FROM packages WHERE packages.scope = scopes.scope AND created_at > now() - '1 week'::interval) AS "usage_new_package_per_week", (SELECT COUNT(created_at) FROM publishing_tasks WHERE publishing_tasks.package_scope = scopes.scope AND created_at > now() - '1 week'::interval) AS "usage_publish_attempts_per_week" @@ -1885,7 +2029,8 @@ impl Database { WHERE dl.scope = package_versions.scope AND dl.package = package_versions.name AND dl.version = package_versions.version) as "package_version_lifetime_download_count!", - users.id as "user_id?", users.name as "user_name?", users.avatar_url as "user_avatar_url?", users.github_id as "user_github_id", users.updated_at as "user_updated_at?", users.created_at as "user_created_at?" + users.id as "user_id?", users.name as "user_name?", users.avatar_url as "user_avatar_url?", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at?", users.created_at as "user_created_at?" FROM package_versions LEFT JOIN users ON package_versions.user_id = users.id WHERE package_versions.scope = $1 AND package_versions.name = $2 @@ -1917,6 +2062,7 @@ impl Database { name: r.user_name.unwrap(), avatar_url: r.user_avatar_url.unwrap(), github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at.unwrap(), created_at: r.user_created_at.unwrap(), }; @@ -2398,7 +2544,8 @@ impl Database { ) -> Result> { sqlx::query!( r#"SELECT scope_members.scope as "scope_member_scope: ScopeName", scope_members.user_id as "scope_member_user_id", scope_members.is_admin as "scope_member_is_admin", scope_members.updated_at as "scope_member_updated_at", scope_members.created_at as "scope_member_created_at", - users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" + users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM scope_members LEFT JOIN users ON scope_members.user_id = users.id WHERE scope = $1 @@ -2418,6 +2565,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -2521,8 +2669,8 @@ impl Database { ) -> Result> { sqlx::query!( r#"SELECT scope_invites.scope as "scope_invite_scope: ScopeName", scope_invites.target_user_id as "scope_invite_target_user_id", scope_invites.requesting_user_id as "scope_invite_requesting_user_id", scope_invites.updated_at as "scope_invite_updated_at", scope_invites.created_at as "scope_invite_created_at", - target_user.id as "target_user_id", target_user.name as "target_user_name", target_user.github_id as "target_user_github_id", target_user.avatar_url as "target_user_avatar_url", target_user.updated_at as "target_user_updated_at", target_user.created_at as "target_user_created_at", - requesting_user.id as "requesting_user_id", requesting_user.name as "requesting_user_name", requesting_user.github_id as "requesting_user_github_id", requesting_user.avatar_url as "requesting_user_avatar_url", requesting_user.updated_at as "requesting_user_updated_at", requesting_user.created_at as "requesting_user_created_at" + target_user.id as "target_user_id", target_user.name as "target_user_name", target_user.github_id as "target_user_github_id", target_user.gitlab_id as "target_user_gitlab_id", target_user.avatar_url as "target_user_avatar_url", target_user.updated_at as "target_user_updated_at", target_user.created_at as "target_user_created_at", + requesting_user.id as "requesting_user_id", requesting_user.name as "requesting_user_name", requesting_user.github_id as "requesting_user_github_id", requesting_user.gitlab_id as "requesting_user_gitlab_id", requesting_user.avatar_url as "requesting_user_avatar_url", requesting_user.updated_at as "requesting_user_updated_at", requesting_user.created_at as "requesting_user_created_at" FROM scope_invites LEFT JOIN users AS target_user ON scope_invites.target_user_id = target_user.id LEFT JOIN users AS requesting_user ON scope_invites.requesting_user_id = requesting_user.id @@ -2542,6 +2690,7 @@ impl Database { name: r.target_user_name, avatar_url: r.target_user_avatar_url, github_id: r.target_user_github_id, + gitlab_id: r.target_user_gitlab_id, updated_at: r.target_user_updated_at, created_at: r.target_user_created_at, }; @@ -2550,6 +2699,7 @@ impl Database { name: r.requesting_user_name, avatar_url: r.requesting_user_avatar_url, github_id: r.requesting_user_github_id, + gitlab_id: r.requesting_user_gitlab_id, updated_at: r.requesting_user_updated_at, created_at: r.requesting_user_created_at, }; @@ -2566,8 +2716,8 @@ impl Database { ) -> Result> { sqlx::query!( r#"SELECT scope_invites.scope as "scope_invite_scope: ScopeName", scope_invites.target_user_id as "scope_invite_target_user_id", scope_invites.requesting_user_id as "scope_invite_requesting_user_id", scope_invites.updated_at as "scope_invite_updated_at", scope_invites.created_at as "scope_invite_created_at", - target_user.id as "target_user_id", target_user.name as "target_user_name", target_user.avatar_url as "target_user_avatar_url", target_user.github_id as "target_user_github_id", target_user.updated_at as "target_user_updated_at", target_user.created_at as "target_user_created_at", - requesting_user.id as "requesting_user_id", requesting_user.name as "requesting_user_name", requesting_user.avatar_url as "requesting_user_avatar_url", requesting_user.github_id as "requesting_user_github_id", requesting_user.updated_at as "requesting_user_updated_at", requesting_user.created_at as "requesting_user_created_at" + target_user.id as "target_user_id", target_user.name as "target_user_name", target_user.avatar_url as "target_user_avatar_url", target_user.github_id as "target_user_github_id", target_user.gitlab_id as "target_user_gitlab_id", target_user.updated_at as "target_user_updated_at", target_user.created_at as "target_user_created_at", + requesting_user.id as "requesting_user_id", requesting_user.name as "requesting_user_name", requesting_user.avatar_url as "requesting_user_avatar_url", requesting_user.github_id as "requesting_user_github_id", requesting_user.gitlab_id as "requesting_user_gitlab_id", requesting_user.updated_at as "requesting_user_updated_at", requesting_user.created_at as "requesting_user_created_at" FROM scope_invites LEFT JOIN users AS target_user ON scope_invites.target_user_id = target_user.id LEFT JOIN users AS requesting_user ON scope_invites.requesting_user_id = requesting_user.id @@ -2587,6 +2737,7 @@ impl Database { name: r.target_user_name, avatar_url: r.target_user_avatar_url, github_id: r.target_user_github_id, + gitlab_id: r.target_user_gitlab_id, updated_at: r.target_user_updated_at, created_at: r.target_user_created_at, }; @@ -2595,6 +2746,7 @@ impl Database { name: r.requesting_user_name, avatar_url: r.requesting_user_avatar_url, github_id: r.requesting_user_github_id, + gitlab_id: r.requesting_user_gitlab_id, updated_at: r.requesting_user_updated_at, created_at: r.requesting_user_created_at, }; @@ -2974,6 +3126,7 @@ impl Database { users.name as "user_name?", users.avatar_url as "user_avatar_url?", users.github_id as "user_github_id?", +users.gitlab_id as "user_gitlab_id?", users.updated_at as "user_updated_at?", users.created_at as "user_created_at?" FROM publishing_tasks @@ -3003,6 +3156,7 @@ impl Database { name: r.user_name.unwrap(), avatar_url: r.user_avatar_url.unwrap(), github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at.unwrap(), created_at: r.user_created_at.unwrap(), } @@ -3048,6 +3202,7 @@ impl Database { users.name as "user_name?", users.avatar_url as "user_avatar_url?", users.github_id as "user_github_id?", +users.gitlab_id as "user_gitlab_id?", users.updated_at as "user_updated_at?", users.created_at as "user_created_at?" FROM task @@ -3078,6 +3233,7 @@ impl Database { name: r.user_name.unwrap(), avatar_url: r.user_avatar_url.unwrap(), github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at.unwrap(), created_at: r.user_created_at.unwrap(), } @@ -3147,6 +3303,7 @@ impl Database { users.name as "user_name?", users.avatar_url as "user_avatar_url?", users.github_id as "user_github_id?", + users.gitlab_id as "user_gitlab_id?", users.updated_at as "user_updated_at?", users.created_at as "user_created_at?" FROM publishing_tasks @@ -3173,6 +3330,7 @@ impl Database { name: r.user_name.unwrap(), avatar_url: r.user_avatar_url.unwrap(), github_id: r.user_github_id, + gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at.unwrap(), created_at: r.user_created_at.unwrap(), }); @@ -3222,6 +3380,7 @@ impl Database { users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM publishing_tasks @@ -3287,6 +3446,7 @@ impl Database { users.name as "user_name?", users.avatar_url as "user_avatar_url?", users.github_id as "user_github_id?", +users.gitlab_id as "user_gitlab_id?", users.updated_at as "user_updated_at?", users.created_at as "user_created_at?" FROM publishing_tasks @@ -3317,6 +3477,7 @@ impl Database { name: r.user_name.unwrap(), avatar_url: r.user_avatar_url.unwrap(), github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at.unwrap(), created_at: r.user_created_at.unwrap(), } @@ -3473,6 +3634,75 @@ impl Database { .await } + #[instrument(name = "Database::delete_github_identity", skip(self), err)] + pub async fn delete_github_identity( + &self, + github_id: i64, + ) -> Result { + sqlx::query_as!( + GithubIdentity, + "DELETE FROM github_identities WHERE github_id = $1 + RETURNING github_id, access_token, access_token_expires_at, refresh_token, refresh_token_expires_at, updated_at, created_at", + github_id + ) + .fetch_one(&self.pool) + .await + } + + #[instrument(name = "Database::insert_gitlab_identity", skip( + self, + new_gitlab_identity + ), err, fields(gitlab_identity.gitlab_id = new_gitlab_identity.gitlab_id))] + pub async fn upsert_gitlab_identity( + &self, + new_gitlab_identity: NewGitlabIdentity, + ) -> Result { + sqlx::query_as!( + GitlabIdentity, + "INSERT INTO gitlab_identities (gitlab_id, access_token, access_token_expires_at, refresh_token) VALUES ($1, $2, $3, $4) + ON CONFLICT (gitlab_id) DO + UPDATE SET access_token = $2, access_token_expires_at = $3, refresh_token = $4 + RETURNING gitlab_id, access_token, access_token_expires_at, refresh_token, updated_at, created_at", + new_gitlab_identity.gitlab_id, + new_gitlab_identity.access_token, + new_gitlab_identity.access_token_expires_at, + new_gitlab_identity.refresh_token, + ) + .fetch_one(&self.pool) + .await + } + + #[instrument(name = "Database::get_gitlab_identity", skip(self), err)] + pub async fn get_gitlab_identity( + &self, + gitlab_id: i64, + ) -> Result { + sqlx::query_as!( + GitlabIdentity, + "SELECT gitlab_id, access_token, access_token_expires_at, refresh_token, updated_at, created_at + FROM gitlab_identities + WHERE gitlab_id = $1", + gitlab_id + ) + .fetch_one(&self.pool) + .await + } + + #[instrument(name = "Database::delete_gitlab_identity", skip(self), err)] + pub async fn delete_gitlab_identity( + &self, + gitlab_id: i64, + ) -> Result { + sqlx::query_as!( + GitlabIdentity, + "DELETE FROM gitlab_identities WHERE gitlab_id = $1 + RETURNING gitlab_id, access_token, access_token_expires_at, refresh_token, updated_at, created_at", + gitlab_id + ) + .fetch_one(&self.pool) + .await + } + #[instrument( name = "Database::insert_token", skip(self, new_token), @@ -4080,6 +4310,7 @@ impl Database { users.email as "user_email", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.is_blocked as "user_is_blocked", users.is_staff as "user_is_staff", users.scope_limit as "user_scope_limit", @@ -4124,6 +4355,7 @@ impl Database { email: r.user_email, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, is_blocked: r.user_is_blocked, is_staff: r.user_is_staff, scope_usage: r.user_scope_usage, @@ -4197,6 +4429,7 @@ impl Database { users.email as "user_email", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.is_blocked as "user_is_blocked", users.is_staff as "user_is_staff", users.scope_limit as "user_scope_limit", @@ -4253,6 +4486,7 @@ impl Database { users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM ticket_messages @@ -4274,6 +4508,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -4328,6 +4563,7 @@ impl Database { users.email as "user_email", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.is_blocked as "user_is_blocked", users.is_staff as "user_is_staff", users.scope_limit as "user_scope_limit", @@ -4372,6 +4608,7 @@ impl Database { email: r.user_email, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, is_blocked: r.user_is_blocked, is_staff: r.user_is_staff, scope_usage: r.user_scope_usage, @@ -4400,6 +4637,7 @@ impl Database { users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM ticket_messages @@ -4421,6 +4659,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -4459,6 +4698,7 @@ impl Database { users.email as "user_email", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", + users.gitlab_id as "user_gitlab_id", users.is_blocked as "user_is_blocked", users.is_staff as "user_is_staff", users.scope_limit as "user_scope_limit", @@ -4501,6 +4741,7 @@ impl Database { email: r.user_email, avatar_url: r.user_avatar_url, github_id: r.user_github_id, + gitlab_id: r.user_gitlab_id, is_blocked: r.user_is_blocked, is_staff: r.user_is_staff, scope_usage: r.user_scope_usage, @@ -4530,6 +4771,7 @@ impl Database { users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM ticket_messages @@ -4551,6 +4793,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -4590,6 +4833,7 @@ impl Database { users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM message @@ -4613,6 +4857,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, + gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -4673,6 +4918,7 @@ impl Database { users.email as "user_email", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.is_blocked as "user_is_blocked", users.is_staff as "user_is_staff", users.scope_limit as "user_scope_limit", @@ -4716,6 +4962,7 @@ impl Database { email: r.user_email, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, is_blocked: r.user_is_blocked, is_staff: r.user_is_staff, scope_usage: r.user_scope_usage, @@ -4742,6 +4989,7 @@ impl Database { users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM ticket_messages @@ -4763,6 +5011,7 @@ impl Database { name: r.user_name, avatar_url: r.user_avatar_url, github_id: r.user_github_id, +gitlab_id: r.user_gitlab_id, updated_at: r.user_updated_at, created_at: r.user_created_at, }; @@ -4804,7 +5053,8 @@ impl Database { audit_logs.action as "audit_log_action", audit_logs.meta as "audit_log_meta", audit_logs.created_at as "audit_log_created_at", - users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" + users.id as "user_id", users.name as "user_name", users.avatar_url as "user_avatar_url", users.github_id as "user_github_id", +users.gitlab_id as "user_gitlab_id", users.updated_at as "user_updated_at", users.created_at as "user_created_at" FROM audit_logs LEFT JOIN users ON audit_logs.actor_id = users.id WHERE (audit_logs.action ILIKE $1 diff --git a/api/src/db/models.rs b/api/src/db/models.rs index 3345db80b..993c0475d 100644 --- a/api/src/db/models.rs +++ b/api/src/db/models.rs @@ -28,6 +28,7 @@ pub struct User { pub updated_at: DateTime, pub created_at: DateTime, pub github_id: Option, + pub gitlab_id: Option, pub is_blocked: bool, pub is_staff: bool, pub scope_usage: i64, @@ -44,6 +45,7 @@ impl FromRow<'_, sqlx::postgres::PgRow> for User { email: try_get_row_or(row, "email", "user_email")?, avatar_url: try_get_row_or(row, "avatar_url", "user_avatar_url")?, github_id: try_get_row_or(row, "github_id", "user_github_id")?, + gitlab_id: try_get_row_or(row, "gitlab_id", "user_gitlab_id")?, is_blocked: try_get_row_or(row, "is_blocked", "user_is_blocked")?, is_staff: try_get_row_or(row, "is_staff", "user_is_staff")?, scope_usage: try_get_row_or(row, "scope_usage", "user_scope_usage")?, @@ -66,6 +68,7 @@ pub struct UserPublic { pub name: String, pub avatar_url: String, pub github_id: Option, + pub gitlab_id: Option, pub updated_at: DateTime, pub created_at: DateTime, } @@ -77,6 +80,7 @@ impl From for UserPublic { name: user.name, avatar_url: user.avatar_url, github_id: user.github_id, + gitlab_id: user.gitlab_id, updated_at: user.updated_at, created_at: user.created_at, } @@ -90,6 +94,7 @@ impl FromRow<'_, sqlx::postgres::PgRow> for UserPublic { name: try_get_row_or(row, "name", "user_name")?, avatar_url: try_get_row_or(row, "avatar_url", "user_avatar_url")?, github_id: try_get_row_or(row, "github_id", "user_github_id")?, + gitlab_id: try_get_row_or(row, "gitlab_id", "user_gitlab_id")?, updated_at: try_get_row_or(row, "created_at", "user_created_at")?, created_at: try_get_row_or(row, "created_at", "user_created_at")?, }) @@ -102,6 +107,7 @@ pub struct NewUser<'s> { pub email: Option<&'s str>, pub avatar_url: &'s str, pub github_id: Option, + pub gitlab_id: Option, pub is_blocked: bool, pub is_staff: bool, } @@ -574,6 +580,35 @@ impl From for NewGithubIdentity { } } +#[derive(Debug, Clone)] +pub struct GitlabIdentity { + pub gitlab_id: i64, + pub access_token: Option, + pub access_token_expires_at: Option>, + pub refresh_token: Option, + pub updated_at: DateTime, + pub created_at: DateTime, +} + +#[derive(Debug, Clone)] +pub struct NewGitlabIdentity { + pub gitlab_id: i64, + pub access_token: Option, + pub access_token_expires_at: Option>, + pub refresh_token: Option, +} + +impl From for NewGitlabIdentity { + fn from(t: GitlabIdentity) -> Self { + Self { + gitlab_id: t.gitlab_id, + access_token: t.access_token, + access_token_expires_at: t.access_token_expires_at, + refresh_token: t.refresh_token, + } + } +} + #[derive(Debug, Clone)] pub struct Token { pub id: Uuid, diff --git a/api/src/db/tests.rs b/api/src/db/tests.rs index eb86c57f4..a2cc13b8b 100644 --- a/api/src/db/tests.rs +++ b/api/src/db/tests.rs @@ -127,6 +127,7 @@ async fn users() { email: None, avatar_url: "", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: true, }; @@ -137,6 +138,7 @@ async fn users() { email: Some("alice@example.com"), avatar_url: "https://example.com/alice.png", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: true, }; @@ -215,6 +217,7 @@ async fn packages() { email: None, avatar_url: "https://example.com/alice.png", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: false, }) @@ -291,6 +294,7 @@ async fn scope_members() { email: None, avatar_url: "https://example.com/bob.png", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: false, }) @@ -322,6 +326,7 @@ async fn scope_members() { email: None, avatar_url: "https://example.com/alice.png", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: false, }) @@ -363,6 +368,7 @@ async fn create_package_version_and_finalize_publishing_task() { name: "Bob", email: None, github_id: None, + gitlab_id: None, is_blocked: false, is_staff: false, avatar_url: "https://example.com/bob.png", @@ -471,6 +477,7 @@ async fn package_files() { email: None, avatar_url: "https://example.com/alice.png", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: false, }) @@ -598,6 +605,7 @@ async fn tokens() { email: Some("alice@example.com"), avatar_url: "https://example.com/alice.png", github_id: None, + gitlab_id: None, is_blocked: false, is_staff: false, }; diff --git a/api/src/docs/bulb.svg b/api/src/docs/bulb.svg index ce97591bf..492db8aa1 100644 --- a/api/src/docs/bulb.svg +++ b/api/src/docs/bulb.svg @@ -1,9 +1,18 @@ - + + d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3" + /> diff --git a/api/src/docs/info-circle.svg b/api/src/docs/info-circle.svg index d5c57db7a..569784c7f 100644 --- a/api/src/docs/info-circle.svg +++ b/api/src/docs/info-circle.svg @@ -1,6 +1,14 @@ - + diff --git a/api/src/docs/warning-message.svg b/api/src/docs/warning-message.svg index 7c3348ed5..d7f58c24d 100644 --- a/api/src/docs/warning-message.svg +++ b/api/src/docs/warning-message.svg @@ -1,9 +1,18 @@ - + + d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" + /> diff --git a/api/src/docs/warning-octagon.svg b/api/src/docs/warning-octagon.svg index 83fe5b8c7..bcc13cb0d 100644 --- a/api/src/docs/warning-octagon.svg +++ b/api/src/docs/warning-octagon.svg @@ -1,9 +1,18 @@ - + + d="M12.802 2.165l5.575 2.389c.48 .206 .863 .589 1.07 1.07l2.388 5.574c.22 .512 .22 1.092 0 1.604l-2.389 5.575c-.206 .48 -.589 .863 -1.07 1.07l-5.574 2.388c-.512 .22 -1.092 .22 -1.604 0l-5.575 -2.389a2.036 2.036 0 0 1 -1.07 -1.07l-2.388 -5.574a2.036 2.036 0 0 1 0 -1.604l2.389 -5.575c.206 -.48 .589 -.863 1.07 -1.07l5.574 -2.388a2.036 2.036 0 0 1 1.604 0z" + /> diff --git a/api/src/docs/warning-triangle.svg b/api/src/docs/warning-triangle.svg index 0a16825d4..531a1d0ad 100644 --- a/api/src/docs/warning-triangle.svg +++ b/api/src/docs/warning-triangle.svg @@ -1,9 +1,18 @@ - + + d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z" + /> diff --git a/api/src/github.rs b/api/src/external/github.rs similarity index 84% rename from api/src/github.rs rename to api/src/external/github.rs index ad0d31ce6..5ec8590cd 100644 --- a/api/src/github.rs +++ b/api/src/external/github.rs @@ -9,8 +9,6 @@ use crate::util::ApiResult; use crate::util::USER_AGENT; use anyhow::Context; use hyper::StatusCode; -use percent_encoding::AsciiSet; -use percent_encoding::CONTROLS; use serde::Deserialize; use serde::Deserializer; use tracing::error; @@ -72,7 +70,7 @@ impl GitHubUserClient { &self, name: &str, ) -> Result, anyhow::Error> { - let name = sanitize_url_part(name); + let name = super::sanitize_url_part(name); let res = self.request(&format!("/users/{name}")).await?; let status = res.status(); if status == StatusCode::NOT_FOUND { @@ -93,8 +91,8 @@ impl GitHubUserClient { owner: &str, name: &str, ) -> Result, anyhow::Error> { - let owner = sanitize_url_part(owner); - let name = sanitize_url_part(name); + let owner = super::sanitize_url_part(owner); + let name = super::sanitize_url_part(name); let res = self.request(&format!("/repos/{owner}/{name}")).await?; let status = res.status(); if status == StatusCode::NOT_FOUND { @@ -110,6 +108,48 @@ impl GitHubUserClient { } } +pub struct GitHubAppClient { + id: String, + secret: String, +} + +impl GitHubAppClient { + pub fn new(client_id: String, client_secret: String) -> Self { + Self { + id: client_id, + secret: client_secret, + } + } + + #[instrument(name = "GitHubAppClient::delete_authorization", skip(self), err)] + pub async fn delete_authorization( + &self, + access_token: String, + ) -> Result<(), anyhow::Error> { + let res = reqwest::Client::builder() + .user_agent(USER_AGENT) + .build()? + .delete(format!( + "https://api.github.com/applications/{}/grant", + self.id + )) + .basic_auth(&self.id, Some(&self.secret)) + .json(&serde_json::json!({ "access_token": access_token })) + .send() + .await?; + + let status = res.status(); + if status.is_success() { + Ok(()) + } else { + let response = res.text().await?; + Err(anyhow::anyhow!( + "failed to delete authorization (status {status}): {response}" + )) + } + } +} + #[derive(Debug, Deserialize, Clone, Eq, PartialEq)] pub struct User { pub id: i64, @@ -118,7 +158,6 @@ pub struct User { pub avatar_url: String, pub created_at: Option>, pub email: Option, - pub company: Option, } #[derive(Deserialize)] @@ -147,17 +186,6 @@ pub struct RepositoryPermissions { pub push: bool, } -/// https://url.spec.whatwg.org/#fragment-percent-encode-set -const FRAGMENT: &AsciiSet = - &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); - -/// https://url.spec.whatwg.org/#path-percent-encode-set -const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); - -fn sanitize_url_part(part: &str) -> String { - percent_encoding::percent_encode(part.as_bytes(), PATH).to_string() -} - fn deserialize_number_from_string<'de, T, D>( deserializer: D, ) -> Result diff --git a/api/src/external/gitlab.rs b/api/src/external/gitlab.rs new file mode 100644 index 000000000..bd3a7b6c2 --- /dev/null +++ b/api/src/external/gitlab.rs @@ -0,0 +1,144 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +// Copyright Deno Land Inc. All Rights Reserved. Proprietary and confidential. + +use crate::util::USER_AGENT; +use hyper::StatusCode; +use serde::Deserialize; +use tracing::instrument; + +pub struct GitLabUserClient { + access_token: String, +} + +impl GitLabUserClient { + pub fn new(access_token: String) -> Self { + Self { access_token } + } + + async fn request( + &self, + path: &str, + ) -> Result { + let response = reqwest::Client::builder() + .user_agent(USER_AGENT) + .build()? + .get(format!("https://gitlab.com/api/v4{}", path)) + .bearer_auth(&self.access_token) + .send() + .await?; + Ok(response) + } + + #[instrument(name = "GitLabUserClient::current_user", skip(self), err)] + pub async fn current_user(&self) -> Result { + let res = self.request("/user").await?; + let status = res.status(); + if status.is_success() { + Ok(res.json().await?) + } else { + let response = res.text().await?; + Err(anyhow::anyhow!( + "failed to get current user (status {status}): {response}" + )) + } + } + + #[instrument(name = "GitLabUserClient::emails", skip(self), err)] + pub async fn emails(&self) -> Result, anyhow::Error> { + let res = self.request("/user/emails").await?; + let status = res.status(); + if status.is_success() { + Ok(res.json().await?) + } else { + let response = res.text().await?; + Err(anyhow::anyhow!( + "failed to get user emails (status {status}): {response}" + )) + } + } + + #[instrument(name = "GitLabUserClient::get_user", skip(self), err)] + pub async fn get_user( + &self, + name: &str, + ) -> Result, anyhow::Error> { + let name = super::sanitize_url_part(name); + let res = self.request(&format!("/users?username={name}")).await?; + let status = res.status(); + if status == StatusCode::NOT_FOUND { + Ok(None) + } else if status.is_success() { + let user_list: Vec = res.json().await?; + Ok(Some(user_list.into_iter().next().unwrap())) + } else { + let response = res.text().await?; + Err(anyhow::anyhow!( + "failed to get user '{name}' (status {status}): {response}" + )) + } + } + + /*#[instrument(name = "GitLabUserClient::get_repo", skip(self), err)] + pub async fn get_repo( + &self, + owner: &str, + name: &str, + ) -> Result, anyhow::Error> { + let owner = super::sanitize_url_part(owner); + let name = super::sanitize_url_part(name); + let res = self.request(&format!("/projects/{owner}%2F{name}")).await?; + let status = res.status(); + if status == StatusCode::NOT_FOUND { + return Ok(None); + } else if !status.is_success() { + let response = res.text().await?; + return Err(anyhow::anyhow!( + "failed to get repository '{owner}/{name}' (status {status}): {response}", + )); + } + let repo: Repository = res.json().await?; + Ok(Some(repo)) + }*/ +} + +#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] +pub struct User { + pub id: i64, + pub username: String, + pub name: String, + pub avatar_url: String, + pub created_at: Option>, + pub email: Option, +} + +#[derive(Deserialize)] +pub struct Email { + pub email: String, + pub confirmed_at: Option>, +} +/* +#[derive(Deserialize)] +pub struct Repository { + pub id: i64, + pub name: String, + pub owner: RepositoryOwner, + pub visibility: String, + pub permissions: RepositoryPermissions, +} + +#[derive(Deserialize)] +pub struct RepositoryOwner { + pub username: String, +} + +#[derive(Deserialize)] +pub struct RepositoryPermissions { + pub project_access: Option, + pub group_access: Option, +} + +#[derive(Deserialize)] +pub struct RepositoryAccess { + pub access_level: u8, +} +*/ diff --git a/api/src/external/mod.rs b/api/src/external/mod.rs new file mode 100644 index 000000000..d4373a524 --- /dev/null +++ b/api/src/external/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +use percent_encoding::AsciiSet; +use percent_encoding::CONTROLS; + +pub mod github; +pub mod gitlab; +pub mod orama; + +/// https://url.spec.whatwg.org/#fragment-percent-encode-set +const FRAGMENT: &AsciiSet = + &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); + +/// https://url.spec.whatwg.org/#path-percent-encode-set +const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); + +fn sanitize_url_part(part: &str) -> String { + percent_encoding::percent_encode(part.as_bytes(), PATH).to_string() +} diff --git a/api/src/orama.rs b/api/src/external/orama.rs similarity index 100% rename from api/src/orama.rs rename to api/src/external/orama.rs diff --git a/api/src/main.rs b/api/src/main.rs index 861ba02e4..35beda13e 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -8,14 +8,13 @@ mod db; mod docs; mod emails; mod errors_internal; +mod external; mod gcp; mod gcs_paths; -mod github; mod iam; mod ids; mod metadata; mod npm; -mod orama; mod provenance; mod publish; mod sitemap; @@ -31,15 +30,14 @@ mod util; use crate::api::ApiError; use crate::api::PublishQueue; use crate::api::api_router; -use crate::auth::GithubOauth2Client; use crate::buckets::BucketWithQueue; use crate::buckets::Buckets; use crate::config::Config; use crate::db::Database; use crate::emails::EmailSender; use crate::errors_internal::error_handler; +use crate::external::orama::OramaClient; use crate::gcp::Queue; -use crate::orama::OramaClient; use crate::sitemap::packages_sitemap_handler; use crate::sitemap::scopes_sitemap_handler; use crate::sitemap::sitemap_index_handler; @@ -61,7 +59,8 @@ use url::Url; pub struct MainRouterOptions { database: Database, buckets: Buckets, - github_client: GithubOauth2Client, + github_client: auth::github::Oauth2Client, + gitlab_client: auth::gitlab::Oauth2Client, orama_client: Option, email_sender: Option, registry_url: Url, @@ -81,6 +80,7 @@ pub(crate) fn main_router( database, buckets, github_client, + gitlab_client, orama_client, email_sender, registry_url, @@ -96,6 +96,7 @@ pub(crate) fn main_router( .data(database) .data(buckets) .data(github_client) + .data(gitlab_client) .data(orama_client) .data(email_sender) .data(RegistryUrl(registry_url)) @@ -112,9 +113,18 @@ pub(crate) fn main_router( .get("/sitemap.xml", sitemap_index_handler) .get("/sitemap-scopes.xml", scopes_sitemap_handler) .get("/sitemap-packages.xml", packages_sitemap_handler) - .get("/login", auth::login_handler) - .get("/login/callback", auth::login_callback_handler) + .get("/login/:service", auth::login_handler) + .get("/login/callback/:service", auth::login_callback_handler) .get("/logout", auth::logout_handler) + .get("/connect/:service", util::full_auth(auth::connect_handler)) + .get( + "/connect/callback/:service", + util::full_auth(auth::connect_callback_handler), + ) + .get( + "/disconnect/:service", + util::full_auth(auth::disconnect_handler), + ) } else { builder }; @@ -201,19 +211,15 @@ async fn main() { ) }); - let github_client = GithubOauth2Client::new( - oauth2::ClientId::new(config.github_client_id), - Some(oauth2::ClientSecret::new(config.github_client_secret)), - oauth2::AuthUrl::new( - "https://github.com/login/oauth/authorize".to_string(), - ) - .unwrap(), - Some( - oauth2::TokenUrl::new( - "https://github.com/login/oauth/access_token".to_string(), - ) - .unwrap(), - ), + let github_client = auth::github::Oauth2Client::new( + config.github_client_id, + config.github_client_secret, + ); + + let gitlab_client = auth::gitlab::Oauth2Client::new( + &config.registry_url, + config.gitlab_client_id, + config.gitlab_client_secret, ); let orama_client = if let Some(orama_package_private_api_key) = @@ -250,6 +256,7 @@ async fn main() { database, buckets, github_client, + gitlab_client, orama_client, email_sender, registry_url: config.registry_url, diff --git a/api/src/publish.rs b/api/src/publish.rs index fa8bd9bbd..45a31a731 100644 --- a/api/src/publish.rs +++ b/api/src/publish.rs @@ -18,6 +18,7 @@ use crate::db::PackageVersionMeta; use crate::db::PublishingTask; use crate::db::PublishingTaskError; use crate::db::PublishingTaskStatus; +use crate::external::orama::OramaClient; use crate::gcp::CACHE_CONTROL_DO_NOT_CACHE; use crate::gcp::CACHE_CONTROL_IMMUTABLE; use crate::gcp::GcsUploadOptions; @@ -27,7 +28,6 @@ use crate::metadata::PackageMetadata; use crate::metadata::VersionMetadata; use crate::npm::NPM_TARBALL_REVISION; use crate::npm::generate_npm_version_manifest; -use crate::orama::OramaClient; use crate::tarball::NpmTarballInfo; use crate::tarball::ProcessTarballOutput; use crate::tarball::process_tarball; diff --git a/api/src/util.rs b/api/src/util.rs index 227488088..1660c4b88 100644 --- a/api/src/util.rs +++ b/api/src/util.rs @@ -25,7 +25,7 @@ use uuid::Uuid; use crate::api::ApiError; use crate::db::Database; use crate::db::Permissions; -use crate::github::verify_oidc_token; +use crate::external::github::verify_oidc_token; use crate::iam::IamInfo; use crate::iam::ReqIamExt as _; use crate::ids::PackageName; @@ -205,6 +205,27 @@ pub async fn auth_middleware(req: Request) -> ApiResult> { Ok(req) } +pub fn full_auth( + handler: H, +) -> impl Fn(Request) -> ApiHandlerFuture> +where + H: Send + Sync + Fn(Request) -> HF + Send + 'static, + HF: Future>> + Send + 'static, +{ + let handler = Arc::new(auth(handler)); + + move |req: Request| { + let handler = handler.clone(); + + async move { + let req = auth_middleware(req).await?; + let res = handler(req).await?; + Ok(res) + } + .boxed() + } +} + enum AuthorizationToken<'s> { Bearer(&'s str), GithubOIDC(&'s str), @@ -338,7 +359,7 @@ pub trait RequestIdExt { fn param_version_or_latest(&self) -> Result; } -fn param<'a>( +pub fn param<'a>( req: &'a Request, name: &str, ) -> Result<&'a String, ApiError> { @@ -419,12 +440,14 @@ impl RequestIdExt for Request { pub mod test { use crate::ApiError; use crate::MainRouterOptions; - use crate::auth::GithubOauth2Client; use crate::buckets::BucketWithQueue; use crate::buckets::Buckets; + use crate::db::Database; use crate::db::EphemeralDatabase; use crate::db::NewGithubIdentity; - use crate::db::{Database, NewUser, User}; + use crate::db::NewGitlabIdentity; + use crate::db::NewUser; + use crate::db::User; use crate::errors_internal::ApiErrorStruct; use crate::gcp::FakeGcsTester; use crate::ids::ScopeDescription; @@ -461,7 +484,9 @@ pub mod test { #[allow(dead_code)] pub scope: crate::db::Scope, #[allow(dead_code)] - pub github_oauth2_client: GithubOauth2Client, + pub github_oauth2_client: crate::auth::github::Oauth2Client, + #[allow(dead_code)] + pub gitlab_oauth2_client: crate::auth::gitlab::Oauth2Client, pub service: RequestService, } @@ -480,19 +505,13 @@ pub mod test { docs_bucket: BucketWithQueue::new(docs_bucket), npm_bucket: BucketWithQueue::new(npm_bucket), }; - let github_oauth2_client = GithubOauth2Client::new( - oauth2::ClientId::new("".to_string()), - Some(oauth2::ClientSecret::new("".to_string())), - oauth2::AuthUrl::new( - "https://github.com/login/oauth/authorize".to_string(), - ) - .unwrap(), - Some( - oauth2::TokenUrl::new( - "https://github.com/login/oauth/access_token".to_string(), - ) - .unwrap(), - ), + let registry_url = "http://jsr-tests.test".parse().unwrap(); + let github_oauth2_client = + crate::auth::github::Oauth2Client::new("".to_string(), "".to_string()); + let gitlab_oauth2_client = crate::auth::gitlab::Oauth2Client::new( + ®istry_url, + "".to_string(), + "".to_string(), ); let user1 = Self::create_user( @@ -502,6 +521,7 @@ pub mod test { email: None, avatar_url: "https://avatars0.githubusercontent.com/u/952?v=4", github_id: Some(101), + gitlab_id: Some(101), is_blocked: false, is_staff: false, }, @@ -516,6 +536,7 @@ pub mod test { email: None, avatar_url: "", github_id: Some(102), + gitlab_id: Some(102), is_blocked: false, is_staff: false, }, @@ -530,6 +551,7 @@ pub mod test { email: None, avatar_url: "", github_id: Some(103), + gitlab_id: Some(103), is_blocked: false, is_staff: false, }, @@ -544,6 +566,7 @@ pub mod test { email: None, avatar_url: "", github_id: Some(104), + gitlab_id: Some(104), is_blocked: false, is_staff: true, }, @@ -579,9 +602,10 @@ pub mod test { database: db, buckets: buckets.clone(), github_client: github_oauth2_client.clone(), + gitlab_client: gitlab_oauth2_client.clone(), orama_client: None, email_sender: None, - registry_url: "http://jsr-tests.test".parse().unwrap(), + registry_url, npm_url: "http://npm.jsr-tests.test".parse().unwrap(), publish_queue: None, // no queue locally npm_tarball_build_queue: None, // no queue locally @@ -604,6 +628,7 @@ pub mod test { staff_user, scope, github_oauth2_client, + gitlab_oauth2_client, service, } } @@ -622,6 +647,14 @@ pub mod test { }) .await .unwrap(); + db.upsert_gitlab_identity(NewGitlabIdentity { + gitlab_id: new_user.gitlab_id.unwrap(), + access_token: None, + access_token_expires_at: None, + refresh_token: None, + }) + .await + .unwrap(); let user = db.insert_user(new_user).await.unwrap(); diff --git a/deno.json b/deno.json index fc784d351..2cae5eff7 100644 --- a/deno.json +++ b/deno.json @@ -6,7 +6,7 @@ "services:linux-no-postgres": "docker compose up jaeger & ./tools/bin/linux-amd64/fake-gcs-server -scheme http -port 4080 -filesystem-root=.gcs & ./tools/server.ts", "dev:api": "cd api && cargo run", "dev:frontend": "cd frontend && OLTP_ENDPOINT=http://localhost:4318 deno task dev", - "prod:frontend": "deno run -A --watch ./tools/prod_proxy.ts & cd frontend && API_ROOT=https://api.jsr.io deno task dev", + "prod:frontend": "deno run -A --watch ./tools/prod_proxy.ts & cd frontend && PROD_PROXY=1 API_ROOT=https://api.jsr.io deno task dev", "lint": "deno task lint:frontend && deno task lint:tools && deno task lint:license", "lint:frontend": "cd frontend && deno lint && deno check --allow-import=googleapis.deno.dev,deno.land,jsr.io dev.ts main.ts routes/**/*.tsx routes/**/*.ts", "lint:tools": "deno lint tools/ && deno check --allow-import=googleapis.deno.dev,deno.land,jsr.io tools/**/*.ts", diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 89f3c6102..734170997 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -3,10 +3,10 @@ import { FullUser } from "../utils/api_types.ts"; import { GlobalSearch } from "../islands/GlobalSearch.tsx"; import { UserMenu } from "../islands/UserMenu.tsx"; -import TbBrandGithub from "tb-icons/TbBrandGithub"; import { SearchKind } from "../util.ts"; import { HeaderLogo } from "../islands/HeaderLogo.tsx"; import DarkModeToggle from "../islands/DarkModeToggle.tsx"; +import { SignInMenu } from "../islands/SignInMenu.tsx"; export function Header({ user, @@ -20,7 +20,6 @@ export function Header({ searchKind?: SearchKind; }) { const redirectUrl = `${url.pathname}${url.search}${url.hash}`; - const loginUrl = `/login?redirect=${encodeURIComponent(redirectUrl)}`; const logoutUrl = `/logout?redirect=${encodeURIComponent(redirectUrl)}`; const oramaPackageApiKey = Deno.env.get("ORAMA_PACKAGE_PUBLIC_API_KEY"); @@ -105,15 +104,18 @@ export function Header({ )} - - {user - ? - : ( - - - Sign in - - )} + {url.pathname !== "/login" && ( + <> + + {user + ? + : ( + + )} + + )}
diff --git a/frontend/islands/GitHubActionsLink.tsx b/frontend/islands/GitHubActionsLink.tsx index 90affbcb0..1a2a588d9 100644 --- a/frontend/islands/GitHubActionsLink.tsx +++ b/frontend/islands/GitHubActionsLink.tsx @@ -15,7 +15,7 @@ export function GitHubActionsLink( const error = useSignal(""); useEffect(() => { - if (user) { + if (user && user.githubId) { cachedGitHubLogin(user) .then((login) => { if (owner.value == "") owner.value = login; diff --git a/frontend/islands/GitLabUserLink.tsx b/frontend/islands/GitLabUserLink.tsx new file mode 100644 index 000000000..1cf01e225 --- /dev/null +++ b/frontend/islands/GitLabUserLink.tsx @@ -0,0 +1,65 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +import { useSignal } from "@preact/signals"; +import TbBrandGitlab from "tb-icons/TbBrandGitlab"; +import { useEffect } from "preact/hooks"; +import { User } from "../utils/api_types.ts"; +import { cachedGitLabUsername } from "../utils/gitlab.ts"; + +export function GitLabUserLink({ user }: { user: User }) { + const login = useSignal(""); + const error = useSignal(false); + + useEffect(() => { + if (user.gitlabId !== null) { + cachedGitLabUsername(user) + .then((login_) => { + login.value = login_; + }) + .catch((error_) => { + console.error(error_); + + error.value = true; + }); + } + }); + + const icon = ( + + ); + + if (user.gitlabId === null) { + return ( + + {icon} + account not linked + + ); + } else if (error.value) { + return ( + + {icon} + unavailable + + ); + } else if (login.value == "") { + return ( + + {icon} + loading... + + ); + } else { + return ( + + {icon} + {login.value} + + ); + } +} diff --git a/frontend/islands/GithubUserLink.tsx b/frontend/islands/GithubUserLink.tsx index 744510e99..b03186528 100644 --- a/frontend/islands/GithubUserLink.tsx +++ b/frontend/islands/GithubUserLink.tsx @@ -5,12 +5,12 @@ import { useEffect } from "preact/hooks"; import { User } from "../utils/api_types.ts"; import { cachedGitHubLogin } from "../utils/github.ts"; -export function GitHubUserLink({ user }: { user?: User }) { +export function GitHubUserLink({ user }: { user: User }) { const login = useSignal(""); const error = useSignal(false); useEffect(() => { - if (user) { + if (user.githubId !== null) { cachedGitHubLogin(user) .then((login_) => { login.value = login_; @@ -23,21 +23,43 @@ export function GitHubUserLink({ user }: { user?: User }) { } }); - if (error.value) { + const icon = ( + + ); + + if (user.githubId === null) { return ( - Could not load GitHub username + + {icon} + account not linked + ); - } - - return login.value == "" - ? loading... - : ( + } else if (error.value) { + return ( + + {icon} + unavailable + + ); + } else if (login.value == "") { + return ( + + {icon} + loading... + + ); + } else { + return ( - - GitHub + {icon} + {login.value} ); + } } diff --git a/frontend/islands/SignInMenu.tsx b/frontend/islands/SignInMenu.tsx new file mode 100644 index 000000000..22ecd4585 --- /dev/null +++ b/frontend/islands/SignInMenu.tsx @@ -0,0 +1,69 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +import { useEffect, useId, useRef } from "preact/hooks"; +import { useSignal } from "@preact/signals"; +import { TbLogin2 } from "tb-icons"; +import { asset } from "fresh/runtime"; + +const SHARED_ITEM_CLASSES = + "flex items-center justify-start gap-2 px-4 py-2.5 focus-visible:ring-2 ring-inset outline-none"; + +export function SignInMenu({ redirect }: { redirect: string }) { + const open = useSignal(false); + const ref = useRef(null); + + useEffect(() => { + function outsideClick(e: Event) { + if (ref.current && !ref.current.contains(e.target as Element)) { + open.value = false; + } + } + document.addEventListener("click", outsideClick); + return () => document.removeEventListener("click", outsideClick); + }, []); + + const prefix = useId(); + + return ( +
+ + +
+ ); +} diff --git a/frontend/routes/@[scope]/(_islands)/ScopeInviteForm.tsx b/frontend/routes/@[scope]/(_islands)/ScopeInviteForm.tsx index fe9c9f671..fe38787d9 100644 --- a/frontend/routes/@[scope]/(_islands)/ScopeInviteForm.tsx +++ b/frontend/routes/@[scope]/(_islands)/ScopeInviteForm.tsx @@ -2,18 +2,19 @@ import { useSignal } from "@preact/signals"; import { useCallback, useRef } from "preact/hooks"; import { JSX } from "preact/jsx-runtime"; -import { ScopeInvite } from "../../../utils/api_types.ts"; +import type { FullUser, ScopeInvite } from "../../../utils/api_types.ts"; import { api, path } from "../../../utils/api.ts"; import { TbUsersPlus } from "tb-icons"; interface ScopeInviteFormProps { + currentUser: FullUser; scope: string; } export function ScopeInviteForm(props: ScopeInviteFormProps) { const submitting = useSignal(false); const error = useSignal(""); - const kind = useSignal<"github" | "id">("github"); + const kind = useSignal<"github" | "gitlab" | "id">("github"); const inputRef = useRef(null); const onSubmit = useCallback( @@ -30,6 +31,7 @@ export function ScopeInviteForm(props: ScopeInviteFormProps) { path`/scopes/${props.scope}/members`, { githubLogin: kind === "github" ? inviteValue : undefined, + gitlabUsername: kind === "gitlab" ? inviteValue : undefined, id: kind === "id" ? inviteValue : undefined, }, ).then((res) => { @@ -70,7 +72,26 @@ export function ScopeInviteForm(props: ScopeInviteFormProps) { } }} > - + + (function ScopeMembersPage( /> ))} - {iam.canAdmin && } + {iam.canAdmin && ( + + )} {data.scopeMember && (

Invite member

@@ -192,7 +196,7 @@ function MemberInvite({ scope }: { scope: string }) { in this scope and create new packages. They will not be able to manage members unless they are granted admin status.

- +
); } diff --git a/frontend/routes/account/(_components)/AccountLayout.tsx b/frontend/routes/account/(_components)/AccountLayout.tsx index 385e4159c..2f247f701 100644 --- a/frontend/routes/account/(_components)/AccountLayout.tsx +++ b/frontend/routes/account/(_components)/AccountLayout.tsx @@ -4,6 +4,7 @@ import twas from "twas"; import { AccountNav, AccountNavTab } from "./AccountNav.tsx"; import { FullUser, User } from "../../../utils/api_types.ts"; import { GitHubUserLink } from "../../../islands/GithubUserLink.tsx"; +//import { GitLabUserLink } from "../../../islands/GitLabUserLink.tsx"; interface AccountLayoutProps { user: User | FullUser; @@ -27,8 +28,9 @@ export function AccountLayout({ user, active, children }: AccountLayoutProps) {

Created account {twas(new Date(user.createdAt).getTime())}

-

+

+ {/* TODO: figure out a way to get this working, requires auth tokens to gitlab */}

diff --git a/frontend/routes/account/settings.tsx b/frontend/routes/account/settings.tsx index 18c6fa4be..52553a956 100644 --- a/frontend/routes/account/settings.tsx +++ b/frontend/routes/account/settings.tsx @@ -4,10 +4,14 @@ import { AccountLayout } from "./(_components)/AccountLayout.tsx"; import { QuotaCard } from "../../components/QuotaCard.tsx"; import { define } from "../../util.ts"; import { TicketModal } from "../../islands/TicketModal.tsx"; +import { asset } from "fresh/runtime"; export default define.page(function AccountInvitesPage({ data, }) { + // @ts-ignore this is possible, typescript just doesnt like it. + const connectionsCount = !!data.user.githubId + !!data.user.gitlabId; + return (
@@ -65,6 +69,27 @@ export default define.page(function AccountInvitesPage({
+
+

Connected accounts

+

+ You may connect other services at any point, however at least one + service needs to be connected at any time. +

+
+ + +
+

Delete account

@@ -82,6 +107,34 @@ export default define.page(function AccountInvitesPage({ ); }); +function Connection( + { name, serviceId, id, connectionsCount }: { + name: string; + serviceId: string; + id: number | null; + connectionsCount: number; + }, +) { + if (connectionsCount === 1 && id !== null) { + return ( + + ); + } + + return ( + + + {id === null ? "Connect" : "Disconnect"} {name} + + ); +} + export const handler = define.handlers({ async GET(ctx) { const [currentUser] = await Promise.all([ diff --git a/frontend/static/logo-animated.svg b/frontend/static/logo-animated.svg index ea3ea055f..3e128a7f2 100644 --- a/frontend/static/logo-animated.svg +++ b/frontend/static/logo-animated.svg @@ -1,221 +1,674 @@ - diff --git a/frontend/static/logo-square.svg b/frontend/static/logo-square.svg index 6bf59ed32..f48ca67f3 100644 --- a/frontend/static/logo-square.svg +++ b/frontend/static/logo-square.svg @@ -1,6 +1,12 @@ - + - + diff --git a/frontend/static/logo.svg b/frontend/static/logo.svg index 663024d61..5dde632f4 100644 --- a/frontend/static/logo.svg +++ b/frontend/static/logo.svg @@ -2,5 +2,6 @@ + d="M4 5V1H3v3H2V3H1v2h6V4H5V1h3v1h4v2h-1V3h-1v3H9V2H6v1h2v3H5V5" + /> diff --git a/frontend/static/logos/browsers.svg b/frontend/static/logos/browsers.svg index 6706955e4..c00352575 100644 --- a/frontend/static/logos/browsers.svg +++ b/frontend/static/logos/browsers.svg @@ -1 +1,368 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/bun.svg b/frontend/static/logos/bun.svg index 657621724..ee667afad 100644 --- a/frontend/static/logos/bun.svg +++ b/frontend/static/logos/bun.svg @@ -1 +1,48 @@ - + + + diff --git a/frontend/static/logos/cloudflare-workers.svg b/frontend/static/logos/cloudflare-workers.svg index a292b67ed..90bb4f0dd 100644 --- a/frontend/static/logos/cloudflare-workers.svg +++ b/frontend/static/logos/cloudflare-workers.svg @@ -1 +1,108 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/deno.svg b/frontend/static/logos/deno.svg index 1141eb356..b7a1a7fef 100644 --- a/frontend/static/logos/deno.svg +++ b/frontend/static/logos/deno.svg @@ -1,13 +1,34 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/frontend/static/logos/github.svg b/frontend/static/logos/github.svg new file mode 100644 index 000000000..f80583a3f --- /dev/null +++ b/frontend/static/logos/github.svg @@ -0,0 +1,28 @@ + + + + + + diff --git a/frontend/static/logos/gitlab.svg b/frontend/static/logos/gitlab.svg new file mode 100644 index 000000000..43af4df94 --- /dev/null +++ b/frontend/static/logos/gitlab.svg @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/frontend/static/logos/node.svg b/frontend/static/logos/node.svg index bcb99bf58..adc1bf98b 100644 --- a/frontend/static/logos/node.svg +++ b/frontend/static/logos/node.svg @@ -1 +1,64 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/npm.svg b/frontend/static/logos/npm.svg index 6095a7915..10a824d0e 100644 --- a/frontend/static/logos/npm.svg +++ b/frontend/static/logos/npm.svg @@ -1 +1,14 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/npm_textless.svg b/frontend/static/logos/npm_textless.svg index 401f99831..d75dffd75 100644 --- a/frontend/static/logos/npm_textless.svg +++ b/frontend/static/logos/npm_textless.svg @@ -1 +1,19 @@ -n \ No newline at end of file + + n + diff --git a/frontend/static/logos/orama-dark.svg b/frontend/static/logos/orama-dark.svg index 1868b1c79..f445e4dc8 100644 --- a/frontend/static/logos/orama-dark.svg +++ b/frontend/static/logos/orama-dark.svg @@ -1,88 +1,261 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/static/logos/orama-light.svg b/frontend/static/logos/orama-light.svg index 8c00c8159..311d50e76 100644 --- a/frontend/static/logos/orama-light.svg +++ b/frontend/static/logos/orama-light.svg @@ -1,88 +1,261 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/static/logos/pnpm.svg b/frontend/static/logos/pnpm.svg index 8dfc30b4e..06a6b55f3 100644 --- a/frontend/static/logos/pnpm.svg +++ b/frontend/static/logos/pnpm.svg @@ -1 +1,9 @@ - + + + diff --git a/frontend/static/logos/pnpm_textless.svg b/frontend/static/logos/pnpm_textless.svg index d60132a05..d25619272 100644 --- a/frontend/static/logos/pnpm_textless.svg +++ b/frontend/static/logos/pnpm_textless.svg @@ -1,3 +1,77 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/typescript.svg b/frontend/static/logos/typescript.svg index 54bd0379d..5b19ed47d 100644 --- a/frontend/static/logos/typescript.svg +++ b/frontend/static/logos/typescript.svg @@ -1 +1,13 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/vlt.svg b/frontend/static/logos/vlt.svg index 01bf6bea4..2403dddad 100644 --- a/frontend/static/logos/vlt.svg +++ b/frontend/static/logos/vlt.svg @@ -1,5 +1,20 @@ - - - - + + + + diff --git a/frontend/static/logos/yarn.svg b/frontend/static/logos/yarn.svg index deda3ba00..f7998efff 100644 --- a/frontend/static/logos/yarn.svg +++ b/frontend/static/logos/yarn.svg @@ -1 +1,25 @@ - \ No newline at end of file + + + diff --git a/frontend/static/logos/yarn_textless.svg b/frontend/static/logos/yarn_textless.svg index 86edb188d..0bd04f4b4 100644 --- a/frontend/static/logos/yarn_textless.svg +++ b/frontend/static/logos/yarn_textless.svg @@ -1 +1,16 @@ - \ No newline at end of file + + + diff --git a/frontend/static/opensearch.xml b/frontend/static/opensearch.xml index 8ec8a6c64..5831ff719 100644 --- a/frontend/static/opensearch.xml +++ b/frontend/static/opensearch.xml @@ -1,9 +1,17 @@ - + JSR Search for packages on JSR UTF-8 - https://jsr.io/favicon.ico - + https://jsr.io/favicon.ico + https://jsr.io diff --git a/frontend/utils/api_types.ts b/frontend/utils/api_types.ts index 48a4eb881..3c776ac0a 100644 --- a/frontend/utils/api_types.ts +++ b/frontend/utils/api_types.ts @@ -3,6 +3,7 @@ export interface User { id: string; name: string; githubId: number | null; + gitlabId: number | null; avatarUrl: string; updatedAt: string; createdAt: string; diff --git a/frontend/utils/github.ts b/frontend/utils/github.ts index f48e223df..0481f8df0 100644 --- a/frontend/utils/github.ts +++ b/frontend/utils/github.ts @@ -21,7 +21,7 @@ export async function cachedGitHubLogin(user: User): Promise { response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0" ) { - throw new Error("Github API rate limit exceeded"); + throw new Error("GitHub API rate limit exceeded"); } const data = await response.json(); diff --git a/frontend/utils/gitlab.ts b/frontend/utils/gitlab.ts new file mode 100644 index 000000000..fa2981602 --- /dev/null +++ b/frontend/utils/gitlab.ts @@ -0,0 +1,45 @@ +// Copyright 2024 the JSR authors. All rights reserved. MIT license. +import { User } from "./api_types.ts"; +import { getOrInsertItem } from "./client_cache.ts"; + +export async function cachedGitLabUsername(user: User): Promise { + return await getOrInsertItem( + `gl-username-${user.gitlabId}`, + () => { + const MAX_RETRIES = 3; + const fetchGitlabUser = async (retryCount = 0) => { + const response = await fetch( + `https://gitlab.com/api/v4/users/${user.gitlabId}`, + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if ( + response.status === 403 && + response.headers.get("RateLimit-Remaining") === "0" + ) { + throw new Error("GitLab API rate limit exceeded"); + } + + const data = await response.json(); + + if (!data.username) { + if (retryCount >= MAX_RETRIES) { + throw new Error( + "Failed to fetch GitLab username after maximum retries", + ); + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + return fetchGitlabUser(retryCount + 1); + } + return data.username; + }; + + return fetchGitlabUser(); + }, + ); +} diff --git a/terraform/cloud_run_api.tf b/terraform/cloud_run_api.tf index 7628e9b9f..8d084fe04 100644 --- a/terraform/cloud_run_api.tf +++ b/terraform/cloud_run_api.tf @@ -13,6 +13,9 @@ locals { "GITHUB_CLIENT_ID" = var.github_client_id # GITHUB_CLIENT_SECRET is defined inline, because it comes from Secrets Manager + "GITLAB_CLIENT_ID" = var.gitlab_client_id + # GITLAB_CLIENT_SECRET is defined inline, because it comes from Secrets Manager + # POSTMARK_TOKEN is defined inline, because it comes from Secrets Manager # ORAMA_PACKAGE_PRIVATE_API_KEY is defined inline, because it comes from Secrets Manager @@ -80,6 +83,16 @@ resource "google_cloud_run_v2_service" "registry_api" { } } + env { + name = "GITLAB_CLIENT_SECRET" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.gitlab_client_secret.id + version = "latest" + } + } + } + env { name = "POSTMARK_TOKEN" value_source { @@ -228,6 +241,17 @@ resource "google_cloud_run_v2_service" "registry_api_tasks" { } } + + env { + name = "GITLAB_CLIENT_SECRET" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.gitlab_client_secret.id + version = "latest" + } + } + } + env { name = "ORAMA_PACKAGE_PRIVATE_API_KEY" value_source { @@ -310,6 +334,12 @@ resource "google_secret_manager_secret_iam_member" "github_client_secret" { member = "serviceAccount:${google_service_account.registry_api.email}" } +resource "google_secret_manager_secret_iam_member" "gitlab_client_secret" { + secret_id = google_secret_manager_secret.gitlab_client_secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.registry_api.email}" +} + resource "google_secret_manager_secret_iam_member" "postmark_token" { secret_id = google_secret_manager_secret.postmark_token.id role = "roles/secretmanager.secretAccessor" diff --git a/terraform/https.tf b/terraform/https.tf index 5f766b029..c9d7f40a3 100644 --- a/terraform/https.tf +++ b/terraform/https.tf @@ -120,7 +120,7 @@ resource "google_compute_url_map" "frontend_https" { # # Additionally, any requests originating from the Googlebot user agent are # punched through to the frontend service, never to the modules bucket. - # + # # These restrictions are in place to prevent users from accessing hosted files # in navigation requests, while allowing access to them (even cross-site) when # using `fetch`. We disallow loading resources directly from `` and @@ -160,13 +160,16 @@ resource "google_compute_url_map" "frontend_https" { full_path_match = "/sitemap-packages.xml" } match_rules { - full_path_match = "/login" + prefix_match = "/login" } match_rules { - full_path_match = "/login/callback" + full_path_match = "/logout" } match_rules { - full_path_match = "/logout" + prefix_match = "/connect" + } + match_rules { + prefix_match = "/disconnect" } } diff --git a/terraform/prod.tfvars b/terraform/prod.tfvars index b28e612f5..da46d7bdf 100644 --- a/terraform/prod.tfvars +++ b/terraform/prod.tfvars @@ -6,4 +6,5 @@ orama_package_public_index_id = "jsr-j7uqzz" orama_docs_public_api_key = "q9ikYsh66FPVJHEGydHhyxsIfBSpwCjp" orama_docs_public_index_id = "jsr-docs-hgtqb6" github_client_id = "Iv1.c0627603fa450ec3" +gitlab_client_id = "ffcfde310e3fc7cc2e9061c28f08268664a80b7d8b56011d239c46d7debf6fd8" production = true diff --git a/terraform/secrets.tf b/terraform/secrets.tf index ee70d39cf..d9bf68034 100644 --- a/terraform/secrets.tf +++ b/terraform/secrets.tf @@ -11,6 +11,18 @@ resource "google_secret_manager_secret_version" "github_client_secret" { secret_data = var.github_client_secret } +resource "google_secret_manager_secret" "gitlab_client_secret" { + secret_id = "gitlab-client-secret" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "gitlab_client_secret" { + secret = google_secret_manager_secret.gitlab_client_secret.id + secret_data = var.gitlab_client_secret +} + resource "google_secret_manager_secret" "postmark_token" { secret_id = "postmark-token" replication { diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index 32418b38e..62e8a0d11 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -6,3 +6,4 @@ orama_package_public_index_id = "jsr-staging-x84zqv" orama_docs_public_api_key = "wxhe9MwCJf4ArRUmthZjkR4fisStO2I3" orama_docs_public_index_id = "jsr-docs-staging-y01k25" github_client_id = "Iv1.b30125ad4e70d4c1" +gitlab_client_id = "197c794fd89e3af665a8c146fca0d9a62f33c1c56e48895b578f5a74b11edb4c" diff --git a/terraform/variables.tf b/terraform/variables.tf index 33073003b..28158165f 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -26,6 +26,15 @@ variable "github_client_secret" { sensitive = true } +variable "gitlab_client_id" { + type = string +} + +variable "gitlab_client_secret" { + type = string + sensitive = true +} + variable "postmark_token" { type = string sensitive = true diff --git a/tools/server.ts b/tools/server.ts index e7733d410..45d49dc5b 100755 --- a/tools/server.ts +++ b/tools/server.ts @@ -62,8 +62,9 @@ async function handler(req: Request): Promise { url.pathname === "/sitemap.xml" || url.pathname === "/sitemap-scopes.xml" || url.pathname === "/sitemap-packages.xml" || - url.pathname === "/login" || - url.pathname === "/login/callback" || + url.pathname.startsWith("/login/") || + url.pathname.startsWith("/connect/") || + url.pathname.startsWith("/disconnect/") || url.pathname === "/logout" ) { const apiUrl = `${API_SERVER}${url.pathname}${url.search}`;