From 08b526dfe146e6fa442342742df861122bc7c698 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 16:35:04 +0100 Subject: [PATCH 01/14] feat(security): add project-scoped user api keys --- api/openapi.json | 1322 +++++++++++++++- ent/client.go | 213 ++- ent/ent.go | 2 + ent/hook/hook.go | 12 + ent/migrate/schema.go | 63 + ent/mutation.go | 1409 ++++++++++++++++- ent/predicate/predicate.go | 3 + ent/project.go | 20 +- ent/project/project.go | 30 + ent/project/where.go | 23 + ent/project_create.go | 32 + ent/project_query.go | 76 +- ent/project_update.go | 163 ++ ent/runtime.go | 37 + ent/schema/project.go | 1 + ent/schema/user.go | 1 + ent/schema/userapikey.go | 58 + ent/tx.go | 3 + ent/user.go | 18 +- ent/user/user.go | 30 + ent/user/where.go | 23 + ent/user_create.go | 32 + ent/user_query.go | 76 +- ent/user_update.go | 163 ++ ent/userapikey.go | 315 ++++ ent/userapikey/userapikey.go | 245 +++ ent/userapikey/where.go | 778 +++++++++ ent/userapikey_create.go | 512 ++++++ ent/userapikey_delete.go | 88 + ent/userapikey_query.go | 682 ++++++++ ent/userapikey_update.go | 928 +++++++++++ internal/agentplatform/ports.go | 2 + internal/agentplatform/service.go | 80 +- internal/app/app.go | 4 + internal/domain/agentplatform/contracts.go | 33 +- internal/domain/userapikey/contracts.go | 149 ++ internal/httpapi/agent_platform_api.go | 2 + internal/httpapi/agent_platform_api_test.go | 132 ++ internal/httpapi/authorization.go | 4 +- internal/httpapi/openapi.go | 138 ++ internal/httpapi/security_settings_api.go | 281 +++- .../httpapi/security_settings_requests.go | 21 + internal/httpapi/server.go | 8 + internal/repo/agentplatform/repo.go | 30 + internal/repo/userapikey/repo.go | 205 +++ internal/service/userapikey/service.go | 266 ++++ web/src/lib/api/contracts.ts | 16 + web/src/lib/api/generated/openapi.d.ts | 1032 ++++++++++-- web/src/lib/api/openase.ts | 39 + .../security-settings-user-api-keys.svelte | 341 ++++ .../security-settings-user-api-keys.test.ts | 93 ++ .../components/security-settings.svelte | 5 + .../security-settings.test-helpers.ts | 48 +- .../components/security-settings.test.ts | 76 + .../features/settings/security-settings.ts | 15 + 55 files changed, 10175 insertions(+), 203 deletions(-) create mode 100644 ent/schema/userapikey.go create mode 100644 ent/userapikey.go create mode 100644 ent/userapikey/userapikey.go create mode 100644 ent/userapikey/where.go create mode 100644 ent/userapikey_create.go create mode 100644 ent/userapikey_delete.go create mode 100644 ent/userapikey_query.go create mode 100644 ent/userapikey_update.go create mode 100644 internal/domain/userapikey/contracts.go create mode 100644 internal/repo/userapikey/repo.go create mode 100644 internal/service/userapikey/service.go create mode 100644 web/src/lib/features/settings/components/security-settings-user-api-keys.svelte create mode 100644 web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts diff --git a/api/openapi.json b/api/openapi.json index 69de057c5..1b01cc2fe 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -24346,23 +24346,981 @@ }, "type": "object" }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" + } + }, + "type": "object" + }, "webhooks": { "properties": { "connector_endpoint": { "type": "string" } }, - "type": "object" + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Get project security settings posture response." + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad Request response." + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Not Found response." + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Internal Server Error response." + }, + "502": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad Gateway response." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Service Unavailable response." + } + }, + "summary": "Get project security settings posture", + "tags": [ + "security-settings" + ] + } + }, + "/api/v1/projects/{projectId}/security-settings/api-keys": { + "get": { + "operationId": "listProjectUserAPIKeys", + "parameters": [ + { + "description": "Project ID.", + "in": "path", + "name": "projectId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "api_keys": { + "items": { + "properties": { + "created_at": { + "type": "string" + }, + "disabled_at": { + "nullable": true, + "type": "string" + }, + "expires_at": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "last_used_at": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "revoked_at": { + "nullable": true, + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "status": { + "type": "string" + }, + "token_hint": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "List the current user's project-scoped API keys response." + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad Request response." + }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Unauthorized response." + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Not Found response." + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Internal Server Error response." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Service Unavailable response." + } + }, + "summary": "List the current user's project-scoped API keys", + "tags": [ + "security-settings" + ] + }, + "post": { + "operationId": "createProjectUserAPIKey", + "parameters": [ + { + "description": "Project ID.", + "in": "path", + "name": "projectId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "expires_at": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + } + }, + "description": "Create a new project-scoped user API key for the current human principal request body.", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "api_key": { + "properties": { + "created_at": { + "type": "string" + }, + "disabled_at": { + "nullable": true, + "type": "string" + }, + "expires_at": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "last_used_at": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "revoked_at": { + "nullable": true, + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "status": { + "type": "string" + }, + "token_hint": { + "type": "string" + } + }, + "type": "object" + }, + "plain_text_token": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Create a new project-scoped user API key for the current human principal response." + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad Request response." + }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Unauthorized response." + }, + "403": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Forbidden response." + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Not Found response." + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Internal Server Error response." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Service Unavailable response." + } + }, + "summary": "Create a new project-scoped user API key for the current human principal", + "tags": [ + "security-settings" + ] + } + }, + "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}": { + "delete": { + "operationId": "deleteProjectUserAPIKey", + "parameters": [ + { + "description": "Project ID.", + "in": "path", + "name": "projectId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "API key ID.", + "in": "path", + "name": "keyId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User API key deleted." + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad Request response." + }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Unauthorized response." + }, + "403": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Forbidden response." + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Not Found response." + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Internal Server Error response." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Service Unavailable response." + }, + "default": { + "description": "" + } + }, + "summary": "Delete a project-scoped user API key", + "tags": [ + "security-settings" + ] + } + }, + "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/disable": { + "post": { + "operationId": "disableProjectUserAPIKey", + "parameters": [ + { + "description": "Project ID.", + "in": "path", + "name": "projectId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "API key ID.", + "in": "path", + "name": "keyId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "api_key": { + "properties": { + "created_at": { + "type": "string" + }, + "disabled_at": { + "nullable": true, + "type": "string" + }, + "expires_at": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "last_used_at": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "revoked_at": { + "nullable": true, + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "status": { + "type": "string" + }, + "token_hint": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + }, + "description": "Disable a project-scoped user API key immediately response." + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Bad Request response." + }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Unauthorized response." + }, + "403": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Forbidden response." + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Not Found response." + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Internal Server Error response." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Service Unavailable response." + } + }, + "summary": "Disable a project-scoped user API key immediately", + "tags": [ + "security-settings" + ] + } + }, + "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/rotate": { + "post": { + "operationId": "rotateProjectUserAPIKey", + "parameters": [ + { + "description": "Project ID.", + "in": "path", + "name": "projectId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "API key ID.", + "in": "path", + "name": "keyId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "api_key": { + "properties": { + "created_at": { + "type": "string" + }, + "disabled_at": { + "nullable": true, + "type": "string" + }, + "expires_at": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "last_used_at": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "revoked_at": { + "nullable": true, + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "status": { + "type": "string" + }, + "token_hint": { + "type": "string" } }, "type": "object" + }, + "plain_text_token": { + "type": "string" } }, "type": "object" } } }, - "description": "Get project security settings posture response." + "description": "Rotate a project-scoped user API key and return the new plaintext token once response." }, "400": { "content": { @@ -24382,7 +25340,7 @@ }, "description": "Bad Request response." }, - "404": { + "401": { "content": { "application/json": { "schema": { @@ -24398,9 +25356,9 @@ } } }, - "description": "Not Found response." + "description": "Unauthorized response." }, - "500": { + "403": { "content": { "application/json": { "schema": { @@ -24416,9 +25374,9 @@ } } }, - "description": "Internal Server Error response." + "description": "Forbidden response." }, - "502": { + "404": { "content": { "application/json": { "schema": { @@ -24434,7 +25392,25 @@ } } }, - "description": "Bad Gateway response." + "description": "Not Found response." + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Internal Server Error response." }, "503": { "content": { @@ -24455,7 +25431,7 @@ "description": "Service Unavailable response." } }, - "summary": "Get project security settings posture", + "summary": "Rotate a project-scoped user API key and return the new plaintext token once", "tags": [ "security-settings" ] @@ -24913,8 +25889,62 @@ }, "type": "array" }, - "runtime_secret_responses_redacted": { - "type": "boolean" + "runtime_secret_responses_redacted": { + "type": "boolean" + } + }, + "type": "object" + }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" } }, "type": "object" @@ -25507,6 +26537,60 @@ }, "type": "object" }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" + } + }, + "type": "object" + }, "webhooks": { "properties": { "connector_endpoint": { @@ -26080,6 +27164,60 @@ }, "type": "object" }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" + } + }, + "type": "object" + }, "webhooks": { "properties": { "connector_endpoint": { @@ -26653,6 +27791,60 @@ }, "type": "object" }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" + } + }, + "type": "object" + }, "webhooks": { "properties": { "connector_endpoint": { @@ -27292,6 +28484,60 @@ }, "type": "object" }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" + } + }, + "type": "object" + }, "webhooks": { "properties": { "connector_endpoint": { @@ -28166,6 +29412,60 @@ }, "type": "object" }, + "user_api_keys": { + "properties": { + "allowed_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "allowed_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "supported_scope_groups": { + "items": { + "properties": { + "category": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "supported_scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "token_prefix": { + "type": "string" + } + }, + "type": "object" + }, "webhooks": { "properties": { "connector_endpoint": { diff --git a/ent/client.go b/ent/client.go index b8eb98e07..a69e244d2 100644 --- a/ent/client.go +++ b/ent/client.go @@ -71,6 +71,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/ticketrepoworkspace" "github.com/BetterAndBetterII/openase/ent/ticketstatus" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/usergroupmembership" "github.com/BetterAndBetterII/openase/ent/useridentity" "github.com/BetterAndBetterII/openase/ent/workflow" @@ -194,6 +195,8 @@ type Client struct { TicketStatus *TicketStatusClient // User is the client for interacting with the User builders. User *UserClient + // UserAPIKey is the client for interacting with the UserAPIKey builders. + UserAPIKey *UserAPIKeyClient // UserGroupMembership is the client for interacting with the UserGroupMembership builders. UserGroupMembership *UserGroupMembershipClient // UserIdentity is the client for interacting with the UserIdentity builders. @@ -272,6 +275,7 @@ func (c *Client) init() { c.TicketRepoWorkspace = NewTicketRepoWorkspaceClient(c.config) c.TicketStatus = NewTicketStatusClient(c.config) c.User = NewUserClient(c.config) + c.UserAPIKey = NewUserAPIKeyClient(c.config) c.UserGroupMembership = NewUserGroupMembershipClient(c.config) c.UserIdentity = NewUserIdentityClient(c.config) c.Workflow = NewWorkflowClient(c.config) @@ -425,6 +429,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) { TicketRepoWorkspace: NewTicketRepoWorkspaceClient(cfg), TicketStatus: NewTicketStatusClient(cfg), User: NewUserClient(cfg), + UserAPIKey: NewUserAPIKeyClient(cfg), UserGroupMembership: NewUserGroupMembershipClient(cfg), UserIdentity: NewUserIdentityClient(cfg), Workflow: NewWorkflowClient(cfg), @@ -505,6 +510,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) TicketRepoWorkspace: NewTicketRepoWorkspaceClient(cfg), TicketStatus: NewTicketStatusClient(cfg), User: NewUserClient(cfg), + UserAPIKey: NewUserAPIKeyClient(cfg), UserGroupMembership: NewUserGroupMembershipClient(cfg), UserIdentity: NewUserIdentityClient(cfg), Workflow: NewWorkflowClient(cfg), @@ -555,7 +561,7 @@ func (c *Client) Use(hooks ...Hook) { c.SecretBinding, c.Skill, c.SkillBlob, c.SkillVersion, c.SkillVersionFile, c.Ticket, c.TicketComment, c.TicketCommentRevision, c.TicketDependency, c.TicketExternalLink, c.TicketRepoScope, c.TicketRepoWorkspace, c.TicketStatus, - c.User, c.UserGroupMembership, c.UserIdentity, c.Workflow, + c.User, c.UserAPIKey, c.UserGroupMembership, c.UserIdentity, c.Workflow, c.WorkflowSkillBinding, c.WorkflowVersion, c.WorkspaceInitLease, } { n.Use(hooks...) @@ -581,7 +587,7 @@ func (c *Client) Intercept(interceptors ...Interceptor) { c.SecretBinding, c.Skill, c.SkillBlob, c.SkillVersion, c.SkillVersionFile, c.Ticket, c.TicketComment, c.TicketCommentRevision, c.TicketDependency, c.TicketExternalLink, c.TicketRepoScope, c.TicketRepoWorkspace, c.TicketStatus, - c.User, c.UserGroupMembership, c.UserIdentity, c.Workflow, + c.User, c.UserAPIKey, c.UserGroupMembership, c.UserIdentity, c.Workflow, c.WorkflowSkillBinding, c.WorkflowVersion, c.WorkspaceInitLease, } { n.Intercept(interceptors...) @@ -701,6 +707,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) { return c.TicketStatus.mutate(ctx, m) case *UserMutation: return c.User.mutate(ctx, m) + case *UserAPIKeyMutation: + return c.UserAPIKey.mutate(ctx, m) case *UserGroupMembershipMutation: return c.UserGroupMembership.mutate(ctx, m) case *UserIdentityMutation: @@ -6193,6 +6201,22 @@ func (c *ProjectClient) QueryNotificationRules(_m *Project) *NotificationRuleQue return query } +// QueryUserAPIKeys queries the user_api_keys edge of a Project. +func (c *ProjectClient) QueryUserAPIKeys(_m *Project) *UserAPIKeyQuery { + query := (&UserAPIKeyClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(project.Table, project.FieldID, id), + sqlgraph.To(userapikey.Table, userapikey.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, project.UserAPIKeysTable, project.UserAPIKeysColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + // QueryDefaultAgentProvider queries the default_agent_provider edge of a Project. func (c *ProjectClient) QueryDefaultAgentProvider(_m *Project) *AgentProviderQuery { query := (&AgentProviderClient{config: c.config}).Query() @@ -10808,6 +10832,22 @@ func (c *UserClient) QueryAcceptedOrganizationInvitations(_m *User) *Organizatio return query } +// QueryAPIKeys queries the api_keys edge of a User. +func (c *UserClient) QueryAPIKeys(_m *User) *UserAPIKeyQuery { + query := (&UserAPIKeyClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(user.Table, user.FieldID, id), + sqlgraph.To(userapikey.Table, userapikey.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, user.APIKeysTable, user.APIKeysColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *UserClient) Hooks() []Hook { return c.hooks.User @@ -10833,6 +10873,171 @@ func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) } } +// UserAPIKeyClient is a client for the UserAPIKey schema. +type UserAPIKeyClient struct { + config +} + +// NewUserAPIKeyClient returns a client for the UserAPIKey from the given config. +func NewUserAPIKeyClient(c config) *UserAPIKeyClient { + return &UserAPIKeyClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `userapikey.Hooks(f(g(h())))`. +func (c *UserAPIKeyClient) Use(hooks ...Hook) { + c.hooks.UserAPIKey = append(c.hooks.UserAPIKey, hooks...) +} + +// Intercept adds a list of query interceptors to the interceptors stack. +// A call to `Intercept(f, g, h)` equals to `userapikey.Intercept(f(g(h())))`. +func (c *UserAPIKeyClient) Intercept(interceptors ...Interceptor) { + c.inters.UserAPIKey = append(c.inters.UserAPIKey, interceptors...) +} + +// Create returns a builder for creating a UserAPIKey entity. +func (c *UserAPIKeyClient) Create() *UserAPIKeyCreate { + mutation := newUserAPIKeyMutation(c.config, OpCreate) + return &UserAPIKeyCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of UserAPIKey entities. +func (c *UserAPIKeyClient) CreateBulk(builders ...*UserAPIKeyCreate) *UserAPIKeyCreateBulk { + return &UserAPIKeyCreateBulk{config: c.config, builders: builders} +} + +// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates +// a builder and applies setFunc on it. +func (c *UserAPIKeyClient) MapCreateBulk(slice any, setFunc func(*UserAPIKeyCreate, int)) *UserAPIKeyCreateBulk { + rv := reflect.ValueOf(slice) + if rv.Kind() != reflect.Slice { + return &UserAPIKeyCreateBulk{err: fmt.Errorf("calling to UserAPIKeyClient.MapCreateBulk with wrong type %T, need slice", slice)} + } + builders := make([]*UserAPIKeyCreate, rv.Len()) + for i := 0; i < rv.Len(); i++ { + builders[i] = c.Create() + setFunc(builders[i], i) + } + return &UserAPIKeyCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for UserAPIKey. +func (c *UserAPIKeyClient) Update() *UserAPIKeyUpdate { + mutation := newUserAPIKeyMutation(c.config, OpUpdate) + return &UserAPIKeyUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *UserAPIKeyClient) UpdateOne(_m *UserAPIKey) *UserAPIKeyUpdateOne { + mutation := newUserAPIKeyMutation(c.config, OpUpdateOne, withUserAPIKey(_m)) + return &UserAPIKeyUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *UserAPIKeyClient) UpdateOneID(id uuid.UUID) *UserAPIKeyUpdateOne { + mutation := newUserAPIKeyMutation(c.config, OpUpdateOne, withUserAPIKeyID(id)) + return &UserAPIKeyUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for UserAPIKey. +func (c *UserAPIKeyClient) Delete() *UserAPIKeyDelete { + mutation := newUserAPIKeyMutation(c.config, OpDelete) + return &UserAPIKeyDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *UserAPIKeyClient) DeleteOne(_m *UserAPIKey) *UserAPIKeyDeleteOne { + return c.DeleteOneID(_m.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *UserAPIKeyClient) DeleteOneID(id uuid.UUID) *UserAPIKeyDeleteOne { + builder := c.Delete().Where(userapikey.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &UserAPIKeyDeleteOne{builder} +} + +// Query returns a query builder for UserAPIKey. +func (c *UserAPIKeyClient) Query() *UserAPIKeyQuery { + return &UserAPIKeyQuery{ + config: c.config, + ctx: &QueryContext{Type: TypeUserAPIKey}, + inters: c.Interceptors(), + } +} + +// Get returns a UserAPIKey entity by its id. +func (c *UserAPIKeyClient) Get(ctx context.Context, id uuid.UUID) (*UserAPIKey, error) { + return c.Query().Where(userapikey.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *UserAPIKeyClient) GetX(ctx context.Context, id uuid.UUID) *UserAPIKey { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// QueryUser queries the user edge of a UserAPIKey. +func (c *UserAPIKeyClient) QueryUser(_m *UserAPIKey) *UserQuery { + query := (&UserClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(userapikey.Table, userapikey.FieldID, id), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, userapikey.UserTable, userapikey.UserColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// QueryProject queries the project edge of a UserAPIKey. +func (c *UserAPIKeyClient) QueryProject(_m *UserAPIKey) *ProjectQuery { + query := (&ProjectClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(userapikey.Table, userapikey.FieldID, id), + sqlgraph.To(project.Table, project.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, userapikey.ProjectTable, userapikey.ProjectColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + +// Hooks returns the client hooks. +func (c *UserAPIKeyClient) Hooks() []Hook { + return c.hooks.UserAPIKey +} + +// Interceptors returns the client interceptors. +func (c *UserAPIKeyClient) Interceptors() []Interceptor { + return c.inters.UserAPIKey +} + +func (c *UserAPIKeyClient) mutate(ctx context.Context, m *UserAPIKeyMutation) (Value, error) { + switch m.Op() { + case OpCreate: + return (&UserAPIKeyCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdate: + return (&UserAPIKeyUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpUpdateOne: + return (&UserAPIKeyUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx) + case OpDelete, OpDeleteOne: + return (&UserAPIKeyDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx) + default: + return nil, fmt.Errorf("ent: unknown UserAPIKey mutation op: %q", m.Op()) + } +} + // UserGroupMembershipClient is a client for the UserGroupMembership schema. type UserGroupMembershipClient struct { config @@ -11888,7 +12093,7 @@ type ( RoleBinding, ScheduledJob, Secret, SecretBinding, Skill, SkillBlob, SkillVersion, SkillVersionFile, Ticket, TicketComment, TicketCommentRevision, TicketDependency, TicketExternalLink, TicketRepoScope, TicketRepoWorkspace, - TicketStatus, User, UserGroupMembership, UserIdentity, Workflow, + TicketStatus, User, UserAPIKey, UserGroupMembership, UserIdentity, Workflow, WorkflowSkillBinding, WorkflowVersion, WorkspaceInitLease []ent.Hook } inters struct { @@ -11906,7 +12111,7 @@ type ( RoleBinding, ScheduledJob, Secret, SecretBinding, Skill, SkillBlob, SkillVersion, SkillVersionFile, Ticket, TicketComment, TicketCommentRevision, TicketDependency, TicketExternalLink, TicketRepoScope, TicketRepoWorkspace, - TicketStatus, User, UserGroupMembership, UserIdentity, Workflow, + TicketStatus, User, UserAPIKey, UserGroupMembership, UserIdentity, Workflow, WorkflowSkillBinding, WorkflowVersion, WorkspaceInitLease []ent.Interceptor } ) diff --git a/ent/ent.go b/ent/ent.go index e3decc6de..19be716b3 100644 --- a/ent/ent.go +++ b/ent/ent.go @@ -67,6 +67,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/ticketrepoworkspace" "github.com/BetterAndBetterII/openase/ent/ticketstatus" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/usergroupmembership" "github.com/BetterAndBetterII/openase/ent/useridentity" "github.com/BetterAndBetterII/openase/ent/workflow" @@ -188,6 +189,7 @@ func checkColumn(t, c string) error { ticketrepoworkspace.Table: ticketrepoworkspace.ValidColumn, ticketstatus.Table: ticketstatus.ValidColumn, user.Table: user.ValidColumn, + userapikey.Table: userapikey.ValidColumn, usergroupmembership.Table: usergroupmembership.ValidColumn, useridentity.Table: useridentity.ValidColumn, workflow.Table: workflow.ValidColumn, diff --git a/ent/hook/hook.go b/ent/hook/hook.go index 57d772b52..e84f49694 100644 --- a/ent/hook/hook.go +++ b/ent/hook/hook.go @@ -669,6 +669,18 @@ func (f UserFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m) } +// The UserAPIKeyFunc type is an adapter to allow the use of ordinary +// function as UserAPIKey mutator. +type UserAPIKeyFunc func(context.Context, *ent.UserAPIKeyMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f UserAPIKeyFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if mv, ok := m.(*ent.UserAPIKeyMutation); ok { + return f(ctx, mv) + } + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserAPIKeyMutation", m) +} + // The UserGroupMembershipFunc type is an adapter to allow the use of ordinary // function as UserGroupMembership mutator. type UserGroupMembershipFunc func(context.Context, *ent.UserGroupMembershipMutation) (ent.Value, error) diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index dc9bfe1a5..8c8b63b81 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -2599,6 +2599,66 @@ var ( }, }, } + // UserAPIKeysColumns holds the columns for the "user_api_keys" table. + UserAPIKeysColumns = []*schema.Column{ + {Name: "id", Type: field.TypeUUID}, + {Name: "name", Type: field.TypeString}, + {Name: "token_prefix", Type: field.TypeString}, + {Name: "token_hint", Type: field.TypeString}, + {Name: "token_hash", Type: field.TypeString}, + {Name: "scopes", Type: field.TypeJSON}, + {Name: "status", Type: field.TypeEnum, Enums: []string{"active", "disabled", "revoked"}, Default: "active"}, + {Name: "expires_at", Type: field.TypeTime, Nullable: true}, + {Name: "last_used_at", Type: field.TypeTime, Nullable: true}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + {Name: "disabled_at", Type: field.TypeTime, Nullable: true}, + {Name: "revoked_at", Type: field.TypeTime, Nullable: true}, + {Name: "project_id", Type: field.TypeUUID}, + {Name: "user_id", Type: field.TypeUUID}, + } + // UserAPIKeysTable holds the schema information for the "user_api_keys" table. + UserAPIKeysTable = &schema.Table{ + Name: "user_api_keys", + Columns: UserAPIKeysColumns, + PrimaryKey: []*schema.Column{UserAPIKeysColumns[0]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "user_api_keys_projects_user_api_keys", + Columns: []*schema.Column{UserAPIKeysColumns[13]}, + RefColumns: []*schema.Column{ProjectsColumns[0]}, + OnDelete: schema.NoAction, + }, + { + Symbol: "user_api_keys_users_api_keys", + Columns: []*schema.Column{UserAPIKeysColumns[14]}, + RefColumns: []*schema.Column{UsersColumns[0]}, + OnDelete: schema.NoAction, + }, + }, + Indexes: []*schema.Index{ + { + Name: "userapikey_token_hash", + Unique: true, + Columns: []*schema.Column{UserAPIKeysColumns[4]}, + }, + { + Name: "userapikey_project_id_user_id", + Unique: false, + Columns: []*schema.Column{UserAPIKeysColumns[13], UserAPIKeysColumns[14]}, + }, + { + Name: "userapikey_project_id_status", + Unique: false, + Columns: []*schema.Column{UserAPIKeysColumns[13], UserAPIKeysColumns[6]}, + }, + { + Name: "userapikey_expires_at", + Unique: false, + Columns: []*schema.Column{UserAPIKeysColumns[7]}, + }, + }, + } // UserGroupMembershipsColumns holds the columns for the "user_group_memberships" table. UserGroupMembershipsColumns = []*schema.Column{ {Name: "id", Type: field.TypeUUID}, @@ -2962,6 +3022,7 @@ var ( TicketRepoWorkspacesTable, TicketStatusTable, UsersTable, + UserAPIKeysTable, UserGroupMembershipsTable, UserIdentitiesTable, WorkflowsTable, @@ -3062,6 +3123,8 @@ func init() { TicketRepoWorkspacesTable.ForeignKeys[1].RefTable = ProjectReposTable TicketRepoWorkspacesTable.ForeignKeys[2].RefTable = TicketsTable TicketStatusTable.ForeignKeys[0].RefTable = ProjectsTable + UserAPIKeysTable.ForeignKeys[0].RefTable = ProjectsTable + UserAPIKeysTable.ForeignKeys[1].RefTable = UsersTable WorkflowsTable.ForeignKeys[0].RefTable = AgentsTable WorkflowsTable.ForeignKeys[1].RefTable = ProjectsTable WorkflowsTable.ForeignKeys[2].RefTable = WorkflowVersionsTable diff --git a/ent/mutation.go b/ent/mutation.go index a3f4a7147..6bd46b9bd 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -67,6 +67,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/ticketrepoworkspace" "github.com/BetterAndBetterII/openase/ent/ticketstatus" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/usergroupmembership" "github.com/BetterAndBetterII/openase/ent/useridentity" "github.com/BetterAndBetterII/openase/ent/workflow" @@ -143,6 +144,7 @@ const ( TypeTicketRepoWorkspace = "TicketRepoWorkspace" TypeTicketStatus = "TicketStatus" TypeUser = "User" + TypeUserAPIKey = "UserAPIKey" TypeUserGroupMembership = "UserGroupMembership" TypeUserIdentity = "UserIdentity" TypeWorkflow = "Workflow" @@ -35035,6 +35037,9 @@ type ProjectMutation struct { notification_rules map[uuid.UUID]struct{} removednotification_rules map[uuid.UUID]struct{} clearednotification_rules bool + user_api_keys map[uuid.UUID]struct{} + removeduser_api_keys map[uuid.UUID]struct{} + cleareduser_api_keys bool default_agent_provider *uuid.UUID cleareddefault_agent_provider bool done bool @@ -36840,6 +36845,60 @@ func (m *ProjectMutation) ResetNotificationRules() { m.removednotification_rules = nil } +// AddUserAPIKeyIDs adds the "user_api_keys" edge to the UserAPIKey entity by ids. +func (m *ProjectMutation) AddUserAPIKeyIDs(ids ...uuid.UUID) { + if m.user_api_keys == nil { + m.user_api_keys = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.user_api_keys[ids[i]] = struct{}{} + } +} + +// ClearUserAPIKeys clears the "user_api_keys" edge to the UserAPIKey entity. +func (m *ProjectMutation) ClearUserAPIKeys() { + m.cleareduser_api_keys = true +} + +// UserAPIKeysCleared reports if the "user_api_keys" edge to the UserAPIKey entity was cleared. +func (m *ProjectMutation) UserAPIKeysCleared() bool { + return m.cleareduser_api_keys +} + +// RemoveUserAPIKeyIDs removes the "user_api_keys" edge to the UserAPIKey entity by IDs. +func (m *ProjectMutation) RemoveUserAPIKeyIDs(ids ...uuid.UUID) { + if m.removeduser_api_keys == nil { + m.removeduser_api_keys = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.user_api_keys, ids[i]) + m.removeduser_api_keys[ids[i]] = struct{}{} + } +} + +// RemovedUserAPIKeys returns the removed IDs of the "user_api_keys" edge to the UserAPIKey entity. +func (m *ProjectMutation) RemovedUserAPIKeysIDs() (ids []uuid.UUID) { + for id := range m.removeduser_api_keys { + ids = append(ids, id) + } + return +} + +// UserAPIKeysIDs returns the "user_api_keys" edge IDs in the mutation. +func (m *ProjectMutation) UserAPIKeysIDs() (ids []uuid.UUID) { + for id := range m.user_api_keys { + ids = append(ids, id) + } + return +} + +// ResetUserAPIKeys resets all changes to the "user_api_keys" edge. +func (m *ProjectMutation) ResetUserAPIKeys() { + m.user_api_keys = nil + m.cleareduser_api_keys = false + m.removeduser_api_keys = nil +} + // ClearDefaultAgentProvider clears the "default_agent_provider" edge to the AgentProvider entity. func (m *ProjectMutation) ClearDefaultAgentProvider() { m.cleareddefault_agent_provider = true @@ -37310,7 +37369,7 @@ func (m *ProjectMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *ProjectMutation) AddedEdges() []string { - edges := make([]string, 0, 20) + edges := make([]string, 0, 21) if m.organization != nil { edges = append(edges, project.EdgeOrganization) } @@ -37368,6 +37427,9 @@ func (m *ProjectMutation) AddedEdges() []string { if m.notification_rules != nil { edges = append(edges, project.EdgeNotificationRules) } + if m.user_api_keys != nil { + edges = append(edges, project.EdgeUserAPIKeys) + } if m.default_agent_provider != nil { edges = append(edges, project.EdgeDefaultAgentProvider) } @@ -37490,6 +37552,12 @@ func (m *ProjectMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case project.EdgeUserAPIKeys: + ids := make([]ent.Value, 0, len(m.user_api_keys)) + for id := range m.user_api_keys { + ids = append(ids, id) + } + return ids case project.EdgeDefaultAgentProvider: if id := m.default_agent_provider; id != nil { return []ent.Value{*id} @@ -37500,7 +37568,7 @@ func (m *ProjectMutation) AddedIDs(name string) []ent.Value { // RemovedEdges returns all edge names that were removed in this mutation. func (m *ProjectMutation) RemovedEdges() []string { - edges := make([]string, 0, 20) + edges := make([]string, 0, 21) if m.removedrepos != nil { edges = append(edges, project.EdgeRepos) } @@ -37555,6 +37623,9 @@ func (m *ProjectMutation) RemovedEdges() []string { if m.removednotification_rules != nil { edges = append(edges, project.EdgeNotificationRules) } + if m.removeduser_api_keys != nil { + edges = append(edges, project.EdgeUserAPIKeys) + } return edges } @@ -37670,13 +37741,19 @@ func (m *ProjectMutation) RemovedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case project.EdgeUserAPIKeys: + ids := make([]ent.Value, 0, len(m.removeduser_api_keys)) + for id := range m.removeduser_api_keys { + ids = append(ids, id) + } + return ids } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *ProjectMutation) ClearedEdges() []string { - edges := make([]string, 0, 20) + edges := make([]string, 0, 21) if m.clearedorganization { edges = append(edges, project.EdgeOrganization) } @@ -37734,6 +37811,9 @@ func (m *ProjectMutation) ClearedEdges() []string { if m.clearednotification_rules { edges = append(edges, project.EdgeNotificationRules) } + if m.cleareduser_api_keys { + edges = append(edges, project.EdgeUserAPIKeys) + } if m.cleareddefault_agent_provider { edges = append(edges, project.EdgeDefaultAgentProvider) } @@ -37782,6 +37862,8 @@ func (m *ProjectMutation) EdgeCleared(name string) bool { return m.clearedchat_conversations case project.EdgeNotificationRules: return m.clearednotification_rules + case project.EdgeUserAPIKeys: + return m.cleareduser_api_keys case project.EdgeDefaultAgentProvider: return m.cleareddefault_agent_provider } @@ -37863,6 +37945,9 @@ func (m *ProjectMutation) ResetEdge(name string) error { case project.EdgeNotificationRules: m.ResetNotificationRules() return nil + case project.EdgeUserAPIKeys: + m.ResetUserAPIKeys() + return nil case project.EdgeDefaultAgentProvider: m.ResetDefaultAgentProvider() return nil @@ -67151,6 +67236,9 @@ type UserMutation struct { accepted_organization_invitations map[uuid.UUID]struct{} removedaccepted_organization_invitations map[uuid.UUID]struct{} clearedaccepted_organization_invitations bool + api_keys map[uuid.UUID]struct{} + removedapi_keys map[uuid.UUID]struct{} + clearedapi_keys bool done bool oldValue func(context.Context) (*User, error) predicates []predicate.User @@ -67633,6 +67721,60 @@ func (m *UserMutation) ResetAcceptedOrganizationInvitations() { m.removedaccepted_organization_invitations = nil } +// AddAPIKeyIDs adds the "api_keys" edge to the UserAPIKey entity by ids. +func (m *UserMutation) AddAPIKeyIDs(ids ...uuid.UUID) { + if m.api_keys == nil { + m.api_keys = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.api_keys[ids[i]] = struct{}{} + } +} + +// ClearAPIKeys clears the "api_keys" edge to the UserAPIKey entity. +func (m *UserMutation) ClearAPIKeys() { + m.clearedapi_keys = true +} + +// APIKeysCleared reports if the "api_keys" edge to the UserAPIKey entity was cleared. +func (m *UserMutation) APIKeysCleared() bool { + return m.clearedapi_keys +} + +// RemoveAPIKeyIDs removes the "api_keys" edge to the UserAPIKey entity by IDs. +func (m *UserMutation) RemoveAPIKeyIDs(ids ...uuid.UUID) { + if m.removedapi_keys == nil { + m.removedapi_keys = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.api_keys, ids[i]) + m.removedapi_keys[ids[i]] = struct{}{} + } +} + +// RemovedAPIKeys returns the removed IDs of the "api_keys" edge to the UserAPIKey entity. +func (m *UserMutation) RemovedAPIKeysIDs() (ids []uuid.UUID) { + for id := range m.removedapi_keys { + ids = append(ids, id) + } + return +} + +// APIKeysIDs returns the "api_keys" edge IDs in the mutation. +func (m *UserMutation) APIKeysIDs() (ids []uuid.UUID) { + for id := range m.api_keys { + ids = append(ids, id) + } + return +} + +// ResetAPIKeys resets all changes to the "api_keys" edge. +func (m *UserMutation) ResetAPIKeys() { + m.api_keys = nil + m.clearedapi_keys = false + m.removedapi_keys = nil +} + // Where appends a list predicates to the UserMutation builder. func (m *UserMutation) Where(ps ...predicate.User) { m.predicates = append(m.predicates, ps...) @@ -67877,13 +68019,16 @@ func (m *UserMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *UserMutation) AddedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.organization_memberships != nil { edges = append(edges, user.EdgeOrganizationMemberships) } if m.accepted_organization_invitations != nil { edges = append(edges, user.EdgeAcceptedOrganizationInvitations) } + if m.api_keys != nil { + edges = append(edges, user.EdgeAPIKeys) + } return edges } @@ -67903,19 +68048,28 @@ func (m *UserMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case user.EdgeAPIKeys: + ids := make([]ent.Value, 0, len(m.api_keys)) + for id := range m.api_keys { + ids = append(ids, id) + } + return ids } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *UserMutation) RemovedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.removedorganization_memberships != nil { edges = append(edges, user.EdgeOrganizationMemberships) } if m.removedaccepted_organization_invitations != nil { edges = append(edges, user.EdgeAcceptedOrganizationInvitations) } + if m.removedapi_keys != nil { + edges = append(edges, user.EdgeAPIKeys) + } return edges } @@ -67935,19 +68089,28 @@ func (m *UserMutation) RemovedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case user.EdgeAPIKeys: + ids := make([]ent.Value, 0, len(m.removedapi_keys)) + for id := range m.removedapi_keys { + ids = append(ids, id) + } + return ids } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *UserMutation) ClearedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.clearedorganization_memberships { edges = append(edges, user.EdgeOrganizationMemberships) } if m.clearedaccepted_organization_invitations { edges = append(edges, user.EdgeAcceptedOrganizationInvitations) } + if m.clearedapi_keys { + edges = append(edges, user.EdgeAPIKeys) + } return edges } @@ -67959,6 +68122,8 @@ func (m *UserMutation) EdgeCleared(name string) bool { return m.clearedorganization_memberships case user.EdgeAcceptedOrganizationInvitations: return m.clearedaccepted_organization_invitations + case user.EdgeAPIKeys: + return m.clearedapi_keys } return false } @@ -67981,10 +68146,1242 @@ func (m *UserMutation) ResetEdge(name string) error { case user.EdgeAcceptedOrganizationInvitations: m.ResetAcceptedOrganizationInvitations() return nil + case user.EdgeAPIKeys: + m.ResetAPIKeys() + return nil } return fmt.Errorf("unknown User edge %s", name) } +// UserAPIKeyMutation represents an operation that mutates the UserAPIKey nodes in the graph. +type UserAPIKeyMutation struct { + config + op Op + typ string + id *uuid.UUID + name *string + token_prefix *string + token_hint *string + token_hash *string + scopes *[]string + appendscopes []string + status *userapikey.Status + expires_at *time.Time + last_used_at *time.Time + created_at *time.Time + updated_at *time.Time + disabled_at *time.Time + revoked_at *time.Time + clearedFields map[string]struct{} + user *uuid.UUID + cleareduser bool + project *uuid.UUID + clearedproject bool + done bool + oldValue func(context.Context) (*UserAPIKey, error) + predicates []predicate.UserAPIKey +} + +var _ ent.Mutation = (*UserAPIKeyMutation)(nil) + +// userapikeyOption allows management of the mutation configuration using functional options. +type userapikeyOption func(*UserAPIKeyMutation) + +// newUserAPIKeyMutation creates new mutation for the UserAPIKey entity. +func newUserAPIKeyMutation(c config, op Op, opts ...userapikeyOption) *UserAPIKeyMutation { + m := &UserAPIKeyMutation{ + config: c, + op: op, + typ: TypeUserAPIKey, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withUserAPIKeyID sets the ID field of the mutation. +func withUserAPIKeyID(id uuid.UUID) userapikeyOption { + return func(m *UserAPIKeyMutation) { + var ( + err error + once sync.Once + value *UserAPIKey + ) + m.oldValue = func(ctx context.Context) (*UserAPIKey, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().UserAPIKey.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withUserAPIKey sets the old UserAPIKey of the mutation. +func withUserAPIKey(node *UserAPIKey) userapikeyOption { + return func(m *UserAPIKeyMutation) { + m.oldValue = func(context.Context) (*UserAPIKey, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m UserAPIKeyMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m UserAPIKeyMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// SetID sets the value of the id field. Note that this +// operation is only accepted on creation of UserAPIKey entities. +func (m *UserAPIKeyMutation) SetID(id uuid.UUID) { + m.id = &id +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *UserAPIKeyMutation) ID() (id uuid.UUID, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *UserAPIKeyMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []uuid.UUID{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().UserAPIKey.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetUserID sets the "user_id" field. +func (m *UserAPIKeyMutation) SetUserID(u uuid.UUID) { + m.user = &u +} + +// UserID returns the value of the "user_id" field in the mutation. +func (m *UserAPIKeyMutation) UserID() (r uuid.UUID, exists bool) { + v := m.user + if v == nil { + return + } + return *v, true +} + +// OldUserID returns the old "user_id" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldUserID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUserID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUserID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUserID: %w", err) + } + return oldValue.UserID, nil +} + +// ResetUserID resets all changes to the "user_id" field. +func (m *UserAPIKeyMutation) ResetUserID() { + m.user = nil +} + +// SetProjectID sets the "project_id" field. +func (m *UserAPIKeyMutation) SetProjectID(u uuid.UUID) { + m.project = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *UserAPIKeyMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *UserAPIKeyMutation) ResetProjectID() { + m.project = nil +} + +// SetName sets the "name" field. +func (m *UserAPIKeyMutation) SetName(s string) { + m.name = &s +} + +// Name returns the value of the "name" field in the mutation. +func (m *UserAPIKeyMutation) Name() (r string, exists bool) { + v := m.name + if v == nil { + return + } + return *v, true +} + +// OldName returns the old "name" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldName(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldName is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldName requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldName: %w", err) + } + return oldValue.Name, nil +} + +// ResetName resets all changes to the "name" field. +func (m *UserAPIKeyMutation) ResetName() { + m.name = nil +} + +// SetTokenPrefix sets the "token_prefix" field. +func (m *UserAPIKeyMutation) SetTokenPrefix(s string) { + m.token_prefix = &s +} + +// TokenPrefix returns the value of the "token_prefix" field in the mutation. +func (m *UserAPIKeyMutation) TokenPrefix() (r string, exists bool) { + v := m.token_prefix + if v == nil { + return + } + return *v, true +} + +// OldTokenPrefix returns the old "token_prefix" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldTokenPrefix(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTokenPrefix is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTokenPrefix requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTokenPrefix: %w", err) + } + return oldValue.TokenPrefix, nil +} + +// ResetTokenPrefix resets all changes to the "token_prefix" field. +func (m *UserAPIKeyMutation) ResetTokenPrefix() { + m.token_prefix = nil +} + +// SetTokenHint sets the "token_hint" field. +func (m *UserAPIKeyMutation) SetTokenHint(s string) { + m.token_hint = &s +} + +// TokenHint returns the value of the "token_hint" field in the mutation. +func (m *UserAPIKeyMutation) TokenHint() (r string, exists bool) { + v := m.token_hint + if v == nil { + return + } + return *v, true +} + +// OldTokenHint returns the old "token_hint" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldTokenHint(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTokenHint is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTokenHint requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTokenHint: %w", err) + } + return oldValue.TokenHint, nil +} + +// ResetTokenHint resets all changes to the "token_hint" field. +func (m *UserAPIKeyMutation) ResetTokenHint() { + m.token_hint = nil +} + +// SetTokenHash sets the "token_hash" field. +func (m *UserAPIKeyMutation) SetTokenHash(s string) { + m.token_hash = &s +} + +// TokenHash returns the value of the "token_hash" field in the mutation. +func (m *UserAPIKeyMutation) TokenHash() (r string, exists bool) { + v := m.token_hash + if v == nil { + return + } + return *v, true +} + +// OldTokenHash returns the old "token_hash" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldTokenHash(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTokenHash is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTokenHash requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTokenHash: %w", err) + } + return oldValue.TokenHash, nil +} + +// ResetTokenHash resets all changes to the "token_hash" field. +func (m *UserAPIKeyMutation) ResetTokenHash() { + m.token_hash = nil +} + +// SetScopes sets the "scopes" field. +func (m *UserAPIKeyMutation) SetScopes(s []string) { + m.scopes = &s + m.appendscopes = nil +} + +// Scopes returns the value of the "scopes" field in the mutation. +func (m *UserAPIKeyMutation) Scopes() (r []string, exists bool) { + v := m.scopes + if v == nil { + return + } + return *v, true +} + +// OldScopes returns the old "scopes" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldScopes(ctx context.Context) (v []string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldScopes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldScopes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldScopes: %w", err) + } + return oldValue.Scopes, nil +} + +// AppendScopes adds s to the "scopes" field. +func (m *UserAPIKeyMutation) AppendScopes(s []string) { + m.appendscopes = append(m.appendscopes, s...) +} + +// AppendedScopes returns the list of values that were appended to the "scopes" field in this mutation. +func (m *UserAPIKeyMutation) AppendedScopes() ([]string, bool) { + if len(m.appendscopes) == 0 { + return nil, false + } + return m.appendscopes, true +} + +// ResetScopes resets all changes to the "scopes" field. +func (m *UserAPIKeyMutation) ResetScopes() { + m.scopes = nil + m.appendscopes = nil +} + +// SetStatus sets the "status" field. +func (m *UserAPIKeyMutation) SetStatus(u userapikey.Status) { + m.status = &u +} + +// Status returns the value of the "status" field in the mutation. +func (m *UserAPIKeyMutation) Status() (r userapikey.Status, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldStatus(ctx context.Context) (v userapikey.Status, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *UserAPIKeyMutation) ResetStatus() { + m.status = nil +} + +// SetExpiresAt sets the "expires_at" field. +func (m *UserAPIKeyMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *UserAPIKeyMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldExpiresAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (m *UserAPIKeyMutation) ClearExpiresAt() { + m.expires_at = nil + m.clearedFields[userapikey.FieldExpiresAt] = struct{}{} +} + +// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation. +func (m *UserAPIKeyMutation) ExpiresAtCleared() bool { + _, ok := m.clearedFields[userapikey.FieldExpiresAt] + return ok +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *UserAPIKeyMutation) ResetExpiresAt() { + m.expires_at = nil + delete(m.clearedFields, userapikey.FieldExpiresAt) +} + +// SetLastUsedAt sets the "last_used_at" field. +func (m *UserAPIKeyMutation) SetLastUsedAt(t time.Time) { + m.last_used_at = &t +} + +// LastUsedAt returns the value of the "last_used_at" field in the mutation. +func (m *UserAPIKeyMutation) LastUsedAt() (r time.Time, exists bool) { + v := m.last_used_at + if v == nil { + return + } + return *v, true +} + +// OldLastUsedAt returns the old "last_used_at" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldLastUsedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastUsedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastUsedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastUsedAt: %w", err) + } + return oldValue.LastUsedAt, nil +} + +// ClearLastUsedAt clears the value of the "last_used_at" field. +func (m *UserAPIKeyMutation) ClearLastUsedAt() { + m.last_used_at = nil + m.clearedFields[userapikey.FieldLastUsedAt] = struct{}{} +} + +// LastUsedAtCleared returns if the "last_used_at" field was cleared in this mutation. +func (m *UserAPIKeyMutation) LastUsedAtCleared() bool { + _, ok := m.clearedFields[userapikey.FieldLastUsedAt] + return ok +} + +// ResetLastUsedAt resets all changes to the "last_used_at" field. +func (m *UserAPIKeyMutation) ResetLastUsedAt() { + m.last_used_at = nil + delete(m.clearedFields, userapikey.FieldLastUsedAt) +} + +// SetCreatedAt sets the "created_at" field. +func (m *UserAPIKeyMutation) SetCreatedAt(t time.Time) { + m.created_at = &t +} + +// CreatedAt returns the value of the "created_at" field in the mutation. +func (m *UserAPIKeyMutation) CreatedAt() (r time.Time, exists bool) { + v := m.created_at + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "created_at" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "created_at" field. +func (m *UserAPIKeyMutation) ResetCreatedAt() { + m.created_at = nil +} + +// SetUpdatedAt sets the "updated_at" field. +func (m *UserAPIKeyMutation) SetUpdatedAt(t time.Time) { + m.updated_at = &t +} + +// UpdatedAt returns the value of the "updated_at" field in the mutation. +func (m *UserAPIKeyMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updated_at + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updated_at" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updated_at" field. +func (m *UserAPIKeyMutation) ResetUpdatedAt() { + m.updated_at = nil +} + +// SetDisabledAt sets the "disabled_at" field. +func (m *UserAPIKeyMutation) SetDisabledAt(t time.Time) { + m.disabled_at = &t +} + +// DisabledAt returns the value of the "disabled_at" field in the mutation. +func (m *UserAPIKeyMutation) DisabledAt() (r time.Time, exists bool) { + v := m.disabled_at + if v == nil { + return + } + return *v, true +} + +// OldDisabledAt returns the old "disabled_at" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldDisabledAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldDisabledAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldDisabledAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldDisabledAt: %w", err) + } + return oldValue.DisabledAt, nil +} + +// ClearDisabledAt clears the value of the "disabled_at" field. +func (m *UserAPIKeyMutation) ClearDisabledAt() { + m.disabled_at = nil + m.clearedFields[userapikey.FieldDisabledAt] = struct{}{} +} + +// DisabledAtCleared returns if the "disabled_at" field was cleared in this mutation. +func (m *UserAPIKeyMutation) DisabledAtCleared() bool { + _, ok := m.clearedFields[userapikey.FieldDisabledAt] + return ok +} + +// ResetDisabledAt resets all changes to the "disabled_at" field. +func (m *UserAPIKeyMutation) ResetDisabledAt() { + m.disabled_at = nil + delete(m.clearedFields, userapikey.FieldDisabledAt) +} + +// SetRevokedAt sets the "revoked_at" field. +func (m *UserAPIKeyMutation) SetRevokedAt(t time.Time) { + m.revoked_at = &t +} + +// RevokedAt returns the value of the "revoked_at" field in the mutation. +func (m *UserAPIKeyMutation) RevokedAt() (r time.Time, exists bool) { + v := m.revoked_at + if v == nil { + return + } + return *v, true +} + +// OldRevokedAt returns the old "revoked_at" field's value of the UserAPIKey entity. +// If the UserAPIKey object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *UserAPIKeyMutation) OldRevokedAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRevokedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRevokedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRevokedAt: %w", err) + } + return oldValue.RevokedAt, nil +} + +// ClearRevokedAt clears the value of the "revoked_at" field. +func (m *UserAPIKeyMutation) ClearRevokedAt() { + m.revoked_at = nil + m.clearedFields[userapikey.FieldRevokedAt] = struct{}{} +} + +// RevokedAtCleared returns if the "revoked_at" field was cleared in this mutation. +func (m *UserAPIKeyMutation) RevokedAtCleared() bool { + _, ok := m.clearedFields[userapikey.FieldRevokedAt] + return ok +} + +// ResetRevokedAt resets all changes to the "revoked_at" field. +func (m *UserAPIKeyMutation) ResetRevokedAt() { + m.revoked_at = nil + delete(m.clearedFields, userapikey.FieldRevokedAt) +} + +// ClearUser clears the "user" edge to the User entity. +func (m *UserAPIKeyMutation) ClearUser() { + m.cleareduser = true + m.clearedFields[userapikey.FieldUserID] = struct{}{} +} + +// UserCleared reports if the "user" edge to the User entity was cleared. +func (m *UserAPIKeyMutation) UserCleared() bool { + return m.cleareduser +} + +// UserIDs returns the "user" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// UserID instead. It exists only for internal usage by the builders. +func (m *UserAPIKeyMutation) UserIDs() (ids []uuid.UUID) { + if id := m.user; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetUser resets all changes to the "user" edge. +func (m *UserAPIKeyMutation) ResetUser() { + m.user = nil + m.cleareduser = false +} + +// ClearProject clears the "project" edge to the Project entity. +func (m *UserAPIKeyMutation) ClearProject() { + m.clearedproject = true + m.clearedFields[userapikey.FieldProjectID] = struct{}{} +} + +// ProjectCleared reports if the "project" edge to the Project entity was cleared. +func (m *UserAPIKeyMutation) ProjectCleared() bool { + return m.clearedproject +} + +// ProjectIDs returns the "project" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// ProjectID instead. It exists only for internal usage by the builders. +func (m *UserAPIKeyMutation) ProjectIDs() (ids []uuid.UUID) { + if id := m.project; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetProject resets all changes to the "project" edge. +func (m *UserAPIKeyMutation) ResetProject() { + m.project = nil + m.clearedproject = false +} + +// Where appends a list predicates to the UserAPIKeyMutation builder. +func (m *UserAPIKeyMutation) Where(ps ...predicate.UserAPIKey) { + m.predicates = append(m.predicates, ps...) +} + +// WhereP appends storage-level predicates to the UserAPIKeyMutation builder. Using this method, +// users can use type-assertion to append predicates that do not depend on any generated package. +func (m *UserAPIKeyMutation) WhereP(ps ...func(*sql.Selector)) { + p := make([]predicate.UserAPIKey, len(ps)) + for i := range ps { + p[i] = ps[i] + } + m.Where(p...) +} + +// Op returns the operation name. +func (m *UserAPIKeyMutation) Op() Op { + return m.op +} + +// SetOp allows setting the mutation operation. +func (m *UserAPIKeyMutation) SetOp(op Op) { + m.op = op +} + +// Type returns the node type of this mutation (UserAPIKey). +func (m *UserAPIKeyMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *UserAPIKeyMutation) Fields() []string { + fields := make([]string, 0, 14) + if m.user != nil { + fields = append(fields, userapikey.FieldUserID) + } + if m.project != nil { + fields = append(fields, userapikey.FieldProjectID) + } + if m.name != nil { + fields = append(fields, userapikey.FieldName) + } + if m.token_prefix != nil { + fields = append(fields, userapikey.FieldTokenPrefix) + } + if m.token_hint != nil { + fields = append(fields, userapikey.FieldTokenHint) + } + if m.token_hash != nil { + fields = append(fields, userapikey.FieldTokenHash) + } + if m.scopes != nil { + fields = append(fields, userapikey.FieldScopes) + } + if m.status != nil { + fields = append(fields, userapikey.FieldStatus) + } + if m.expires_at != nil { + fields = append(fields, userapikey.FieldExpiresAt) + } + if m.last_used_at != nil { + fields = append(fields, userapikey.FieldLastUsedAt) + } + if m.created_at != nil { + fields = append(fields, userapikey.FieldCreatedAt) + } + if m.updated_at != nil { + fields = append(fields, userapikey.FieldUpdatedAt) + } + if m.disabled_at != nil { + fields = append(fields, userapikey.FieldDisabledAt) + } + if m.revoked_at != nil { + fields = append(fields, userapikey.FieldRevokedAt) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *UserAPIKeyMutation) Field(name string) (ent.Value, bool) { + switch name { + case userapikey.FieldUserID: + return m.UserID() + case userapikey.FieldProjectID: + return m.ProjectID() + case userapikey.FieldName: + return m.Name() + case userapikey.FieldTokenPrefix: + return m.TokenPrefix() + case userapikey.FieldTokenHint: + return m.TokenHint() + case userapikey.FieldTokenHash: + return m.TokenHash() + case userapikey.FieldScopes: + return m.Scopes() + case userapikey.FieldStatus: + return m.Status() + case userapikey.FieldExpiresAt: + return m.ExpiresAt() + case userapikey.FieldLastUsedAt: + return m.LastUsedAt() + case userapikey.FieldCreatedAt: + return m.CreatedAt() + case userapikey.FieldUpdatedAt: + return m.UpdatedAt() + case userapikey.FieldDisabledAt: + return m.DisabledAt() + case userapikey.FieldRevokedAt: + return m.RevokedAt() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *UserAPIKeyMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case userapikey.FieldUserID: + return m.OldUserID(ctx) + case userapikey.FieldProjectID: + return m.OldProjectID(ctx) + case userapikey.FieldName: + return m.OldName(ctx) + case userapikey.FieldTokenPrefix: + return m.OldTokenPrefix(ctx) + case userapikey.FieldTokenHint: + return m.OldTokenHint(ctx) + case userapikey.FieldTokenHash: + return m.OldTokenHash(ctx) + case userapikey.FieldScopes: + return m.OldScopes(ctx) + case userapikey.FieldStatus: + return m.OldStatus(ctx) + case userapikey.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case userapikey.FieldLastUsedAt: + return m.OldLastUsedAt(ctx) + case userapikey.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case userapikey.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + case userapikey.FieldDisabledAt: + return m.OldDisabledAt(ctx) + case userapikey.FieldRevokedAt: + return m.OldRevokedAt(ctx) + } + return nil, fmt.Errorf("unknown UserAPIKey field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *UserAPIKeyMutation) SetField(name string, value ent.Value) error { + switch name { + case userapikey.FieldUserID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserID(v) + return nil + case userapikey.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil + case userapikey.FieldName: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetName(v) + return nil + case userapikey.FieldTokenPrefix: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTokenPrefix(v) + return nil + case userapikey.FieldTokenHint: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTokenHint(v) + return nil + case userapikey.FieldTokenHash: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTokenHash(v) + return nil + case userapikey.FieldScopes: + v, ok := value.([]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetScopes(v) + return nil + case userapikey.FieldStatus: + v, ok := value.(userapikey.Status) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil + case userapikey.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case userapikey.FieldLastUsedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastUsedAt(v) + return nil + case userapikey.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case userapikey.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + case userapikey.FieldDisabledAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetDisabledAt(v) + return nil + case userapikey.FieldRevokedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRevokedAt(v) + return nil + } + return fmt.Errorf("unknown UserAPIKey field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *UserAPIKeyMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *UserAPIKeyMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *UserAPIKeyMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown UserAPIKey numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *UserAPIKeyMutation) ClearedFields() []string { + var fields []string + if m.FieldCleared(userapikey.FieldExpiresAt) { + fields = append(fields, userapikey.FieldExpiresAt) + } + if m.FieldCleared(userapikey.FieldLastUsedAt) { + fields = append(fields, userapikey.FieldLastUsedAt) + } + if m.FieldCleared(userapikey.FieldDisabledAt) { + fields = append(fields, userapikey.FieldDisabledAt) + } + if m.FieldCleared(userapikey.FieldRevokedAt) { + fields = append(fields, userapikey.FieldRevokedAt) + } + return fields +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *UserAPIKeyMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *UserAPIKeyMutation) ClearField(name string) error { + switch name { + case userapikey.FieldExpiresAt: + m.ClearExpiresAt() + return nil + case userapikey.FieldLastUsedAt: + m.ClearLastUsedAt() + return nil + case userapikey.FieldDisabledAt: + m.ClearDisabledAt() + return nil + case userapikey.FieldRevokedAt: + m.ClearRevokedAt() + return nil + } + return fmt.Errorf("unknown UserAPIKey nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *UserAPIKeyMutation) ResetField(name string) error { + switch name { + case userapikey.FieldUserID: + m.ResetUserID() + return nil + case userapikey.FieldProjectID: + m.ResetProjectID() + return nil + case userapikey.FieldName: + m.ResetName() + return nil + case userapikey.FieldTokenPrefix: + m.ResetTokenPrefix() + return nil + case userapikey.FieldTokenHint: + m.ResetTokenHint() + return nil + case userapikey.FieldTokenHash: + m.ResetTokenHash() + return nil + case userapikey.FieldScopes: + m.ResetScopes() + return nil + case userapikey.FieldStatus: + m.ResetStatus() + return nil + case userapikey.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case userapikey.FieldLastUsedAt: + m.ResetLastUsedAt() + return nil + case userapikey.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case userapikey.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + case userapikey.FieldDisabledAt: + m.ResetDisabledAt() + return nil + case userapikey.FieldRevokedAt: + m.ResetRevokedAt() + return nil + } + return fmt.Errorf("unknown UserAPIKey field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *UserAPIKeyMutation) AddedEdges() []string { + edges := make([]string, 0, 2) + if m.user != nil { + edges = append(edges, userapikey.EdgeUser) + } + if m.project != nil { + edges = append(edges, userapikey.EdgeProject) + } + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *UserAPIKeyMutation) AddedIDs(name string) []ent.Value { + switch name { + case userapikey.EdgeUser: + if id := m.user; id != nil { + return []ent.Value{*id} + } + case userapikey.EdgeProject: + if id := m.project; id != nil { + return []ent.Value{*id} + } + } + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *UserAPIKeyMutation) RemovedEdges() []string { + edges := make([]string, 0, 2) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *UserAPIKeyMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *UserAPIKeyMutation) ClearedEdges() []string { + edges := make([]string, 0, 2) + if m.cleareduser { + edges = append(edges, userapikey.EdgeUser) + } + if m.clearedproject { + edges = append(edges, userapikey.EdgeProject) + } + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *UserAPIKeyMutation) EdgeCleared(name string) bool { + switch name { + case userapikey.EdgeUser: + return m.cleareduser + case userapikey.EdgeProject: + return m.clearedproject + } + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *UserAPIKeyMutation) ClearEdge(name string) error { + switch name { + case userapikey.EdgeUser: + m.ClearUser() + return nil + case userapikey.EdgeProject: + m.ClearProject() + return nil + } + return fmt.Errorf("unknown UserAPIKey unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *UserAPIKeyMutation) ResetEdge(name string) error { + switch name { + case userapikey.EdgeUser: + m.ResetUser() + return nil + case userapikey.EdgeProject: + m.ResetProject() + return nil + } + return fmt.Errorf("unknown UserAPIKey edge %s", name) +} + // UserGroupMembershipMutation represents an operation that mutates the UserGroupMembership nodes in the graph. type UserGroupMembershipMutation struct { config diff --git a/ent/predicate/predicate.go b/ent/predicate/predicate.go index ac88cff66..4ca622e4c 100644 --- a/ent/predicate/predicate.go +++ b/ent/predicate/predicate.go @@ -171,6 +171,9 @@ type TicketStatus func(*sql.Selector) // User is the predicate function for user builders. type User func(*sql.Selector) +// UserAPIKey is the predicate function for userapikey builders. +type UserAPIKey func(*sql.Selector) + // UserGroupMembership is the predicate function for usergroupmembership builders. type UserGroupMembership func(*sql.Selector) diff --git a/ent/project.go b/ent/project.go index fb3148728..34f783d74 100644 --- a/ent/project.go +++ b/ent/project.go @@ -97,11 +97,13 @@ type ProjectEdges struct { ChatConversations []*ChatConversation `json:"chat_conversations,omitempty"` // NotificationRules holds the value of the notification_rules edge. NotificationRules []*NotificationRule `json:"notification_rules,omitempty"` + // UserAPIKeys holds the value of the user_api_keys edge. + UserAPIKeys []*UserAPIKey `json:"user_api_keys,omitempty"` // DefaultAgentProvider holds the value of the default_agent_provider edge. DefaultAgentProvider *AgentProvider `json:"default_agent_provider,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [20]bool + loadedTypes [21]bool } // OrganizationOrErr returns the Organization value or an error if the edge @@ -277,12 +279,21 @@ func (e ProjectEdges) NotificationRulesOrErr() ([]*NotificationRule, error) { return nil, &NotLoadedError{edge: "notification_rules"} } +// UserAPIKeysOrErr returns the UserAPIKeys value or an error if the edge +// was not loaded in eager-loading. +func (e ProjectEdges) UserAPIKeysOrErr() ([]*UserAPIKey, error) { + if e.loadedTypes[19] { + return e.UserAPIKeys, nil + } + return nil, &NotLoadedError{edge: "user_api_keys"} +} + // DefaultAgentProviderOrErr returns the DefaultAgentProvider value or an error if the edge // was not loaded in eager-loading, or loaded but was not found. func (e ProjectEdges) DefaultAgentProviderOrErr() (*AgentProvider, error) { if e.DefaultAgentProvider != nil { return e.DefaultAgentProvider, nil - } else if e.loadedTypes[19] { + } else if e.loadedTypes[20] { return nil, &NotFoundError{label: agentprovider.Label} } return nil, &NotLoadedError{edge: "default_agent_provider"} @@ -533,6 +544,11 @@ func (_m *Project) QueryNotificationRules() *NotificationRuleQuery { return NewProjectClient(_m.config).QueryNotificationRules(_m) } +// QueryUserAPIKeys queries the "user_api_keys" edge of the Project entity. +func (_m *Project) QueryUserAPIKeys() *UserAPIKeyQuery { + return NewProjectClient(_m.config).QueryUserAPIKeys(_m) +} + // QueryDefaultAgentProvider queries the "default_agent_provider" edge of the Project entity. func (_m *Project) QueryDefaultAgentProvider() *AgentProviderQuery { return NewProjectClient(_m.config).QueryDefaultAgentProvider(_m) diff --git a/ent/project/project.go b/ent/project/project.go index 56f464c10..6dece3bf4 100644 --- a/ent/project/project.go +++ b/ent/project/project.go @@ -81,6 +81,8 @@ const ( EdgeChatConversations = "chat_conversations" // EdgeNotificationRules holds the string denoting the notification_rules edge name in mutations. EdgeNotificationRules = "notification_rules" + // EdgeUserAPIKeys holds the string denoting the user_api_keys edge name in mutations. + EdgeUserAPIKeys = "user_api_keys" // EdgeDefaultAgentProvider holds the string denoting the default_agent_provider edge name in mutations. EdgeDefaultAgentProvider = "default_agent_provider" // Table holds the table name of the project in the database. @@ -218,6 +220,13 @@ const ( NotificationRulesInverseTable = "notification_rules" // NotificationRulesColumn is the table column denoting the notification_rules relation/edge. NotificationRulesColumn = "project_id" + // UserAPIKeysTable is the table that holds the user_api_keys relation/edge. + UserAPIKeysTable = "user_api_keys" + // UserAPIKeysInverseTable is the table name for the UserAPIKey entity. + // It exists in this package in order to avoid circular dependency with the "userapikey" package. + UserAPIKeysInverseTable = "user_api_keys" + // UserAPIKeysColumn is the table column denoting the user_api_keys relation/edge. + UserAPIKeysColumn = "project_id" // DefaultAgentProviderTable is the table that holds the default_agent_provider relation/edge. DefaultAgentProviderTable = "projects" // DefaultAgentProviderInverseTable is the table name for the AgentProvider entity. @@ -602,6 +611,20 @@ func ByNotificationRules(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption } } +// ByUserAPIKeysCount orders the results by user_api_keys count. +func ByUserAPIKeysCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newUserAPIKeysStep(), opts...) + } +} + +// ByUserAPIKeys orders the results by user_api_keys terms. +func ByUserAPIKeys(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newUserAPIKeysStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} + // ByDefaultAgentProviderField orders the results by default_agent_provider field. func ByDefaultAgentProviderField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -741,6 +764,13 @@ func newNotificationRulesStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.O2M, false, NotificationRulesTable, NotificationRulesColumn), ) } +func newUserAPIKeysStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(UserAPIKeysInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, UserAPIKeysTable, UserAPIKeysColumn), + ) +} func newDefaultAgentProviderStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), diff --git a/ent/project/where.go b/ent/project/where.go index 4134fbf57..8d135fb7e 100644 --- a/ent/project/where.go +++ b/ent/project/where.go @@ -1091,6 +1091,29 @@ func HasNotificationRulesWith(preds ...predicate.NotificationRule) predicate.Pro }) } +// HasUserAPIKeys applies the HasEdge predicate on the "user_api_keys" edge. +func HasUserAPIKeys() predicate.Project { + return predicate.Project(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, UserAPIKeysTable, UserAPIKeysColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasUserAPIKeysWith applies the HasEdge predicate on the "user_api_keys" edge with a given conditions (other predicates). +func HasUserAPIKeysWith(preds ...predicate.UserAPIKey) predicate.Project { + return predicate.Project(func(s *sql.Selector) { + step := newUserAPIKeysStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // HasDefaultAgentProvider applies the HasEdge predicate on the "default_agent_provider" edge. func HasDefaultAgentProvider() predicate.Project { return predicate.Project(func(s *sql.Selector) { diff --git a/ent/project_create.go b/ent/project_create.go index c25a2673e..0f8194899 100644 --- a/ent/project_create.go +++ b/ent/project_create.go @@ -29,6 +29,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/skill" "github.com/BetterAndBetterII/openase/ent/ticket" "github.com/BetterAndBetterII/openase/ent/ticketstatus" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/workflow" "github.com/BetterAndBetterII/openase/internal/domain/githubauth" "github.com/google/uuid" @@ -484,6 +485,21 @@ func (_c *ProjectCreate) AddNotificationRules(v ...*NotificationRule) *ProjectCr return _c.AddNotificationRuleIDs(ids...) } +// AddUserAPIKeyIDs adds the "user_api_keys" edge to the UserAPIKey entity by IDs. +func (_c *ProjectCreate) AddUserAPIKeyIDs(ids ...uuid.UUID) *ProjectCreate { + _c.mutation.AddUserAPIKeyIDs(ids...) + return _c +} + +// AddUserAPIKeys adds the "user_api_keys" edges to the UserAPIKey entity. +func (_c *ProjectCreate) AddUserAPIKeys(v ...*UserAPIKey) *ProjectCreate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _c.AddUserAPIKeyIDs(ids...) +} + // SetDefaultAgentProvider sets the "default_agent_provider" edge to the AgentProvider entity. func (_c *ProjectCreate) SetDefaultAgentProvider(v *AgentProvider) *ProjectCreate { return _c.SetDefaultAgentProviderID(v.ID) @@ -995,6 +1011,22 @@ func (_c *ProjectCreate) createSpec() (*Project, *sqlgraph.CreateSpec) { } _spec.Edges = append(_spec.Edges, edge) } + if nodes := _c.mutation.UserAPIKeysIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } if nodes := _c.mutation.DefaultAgentProviderIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/ent/project_query.go b/ent/project_query.go index 269c78cfc..b27f262f2 100644 --- a/ent/project_query.go +++ b/ent/project_query.go @@ -33,6 +33,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/skill" "github.com/BetterAndBetterII/openase/ent/ticket" "github.com/BetterAndBetterII/openase/ent/ticketstatus" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/workflow" "github.com/google/uuid" ) @@ -63,6 +64,7 @@ type ProjectQuery struct { withUpdateThreads *ProjectUpdateThreadQuery withChatConversations *ChatConversationQuery withNotificationRules *NotificationRuleQuery + withUserAPIKeys *UserAPIKeyQuery withDefaultAgentProvider *AgentProviderQuery // intermediate query (i.e. traversal path). sql *sql.Selector @@ -518,6 +520,28 @@ func (_q *ProjectQuery) QueryNotificationRules() *NotificationRuleQuery { return query } +// QueryUserAPIKeys chains the current query on the "user_api_keys" edge. +func (_q *ProjectQuery) QueryUserAPIKeys() *UserAPIKeyQuery { + query := (&UserAPIKeyClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(project.Table, project.FieldID, selector), + sqlgraph.To(userapikey.Table, userapikey.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, project.UserAPIKeysTable, project.UserAPIKeysColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + // QueryDefaultAgentProvider chains the current query on the "default_agent_provider" edge. func (_q *ProjectQuery) QueryDefaultAgentProvider() *AgentProviderQuery { query := (&AgentProviderClient{config: _q.config}).Query() @@ -751,6 +775,7 @@ func (_q *ProjectQuery) Clone() *ProjectQuery { withUpdateThreads: _q.withUpdateThreads.Clone(), withChatConversations: _q.withChatConversations.Clone(), withNotificationRules: _q.withNotificationRules.Clone(), + withUserAPIKeys: _q.withUserAPIKeys.Clone(), withDefaultAgentProvider: _q.withDefaultAgentProvider.Clone(), // clone intermediate query. sql: _q.sql.Clone(), @@ -967,6 +992,17 @@ func (_q *ProjectQuery) WithNotificationRules(opts ...func(*NotificationRuleQuer return _q } +// WithUserAPIKeys tells the query-builder to eager-load the nodes that are connected to +// the "user_api_keys" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *ProjectQuery) WithUserAPIKeys(opts ...func(*UserAPIKeyQuery)) *ProjectQuery { + query := (&UserAPIKeyClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withUserAPIKeys = query + return _q +} + // WithDefaultAgentProvider tells the query-builder to eager-load the nodes that are connected to // the "default_agent_provider" edge. The optional arguments are used to configure the query builder of the edge. func (_q *ProjectQuery) WithDefaultAgentProvider(opts ...func(*AgentProviderQuery)) *ProjectQuery { @@ -1056,7 +1092,7 @@ func (_q *ProjectQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proj var ( nodes = []*Project{} _spec = _q.querySpec() - loadedTypes = [20]bool{ + loadedTypes = [21]bool{ _q.withOrganization != nil, _q.withRepos != nil, _q.withSkills != nil, @@ -1076,6 +1112,7 @@ func (_q *ProjectQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proj _q.withUpdateThreads != nil, _q.withChatConversations != nil, _q.withNotificationRules != nil, + _q.withUserAPIKeys != nil, _q.withDefaultAgentProvider != nil, } ) @@ -1239,6 +1276,13 @@ func (_q *ProjectQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proj return nil, err } } + if query := _q.withUserAPIKeys; query != nil { + if err := _q.loadUserAPIKeys(ctx, query, nodes, + func(n *Project) { n.Edges.UserAPIKeys = []*UserAPIKey{} }, + func(n *Project, e *UserAPIKey) { n.Edges.UserAPIKeys = append(n.Edges.UserAPIKeys, e) }); err != nil { + return nil, err + } + } if query := _q.withDefaultAgentProvider; query != nil { if err := _q.loadDefaultAgentProvider(ctx, query, nodes, nil, func(n *Project, e *AgentProvider) { n.Edges.DefaultAgentProvider = e }); err != nil { @@ -1817,6 +1861,36 @@ func (_q *ProjectQuery) loadNotificationRules(ctx context.Context, query *Notifi } return nil } +func (_q *ProjectQuery) loadUserAPIKeys(ctx context.Context, query *UserAPIKeyQuery, nodes []*Project, init func(*Project), assign func(*Project, *UserAPIKey)) error { + fks := make([]driver.Value, 0, len(nodes)) + nodeids := make(map[uuid.UUID]*Project) + for i := range nodes { + fks = append(fks, nodes[i].ID) + nodeids[nodes[i].ID] = nodes[i] + if init != nil { + init(nodes[i]) + } + } + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(userapikey.FieldProjectID) + } + query.Where(predicate.UserAPIKey(func(s *sql.Selector) { + s.Where(sql.InValues(s.C(project.UserAPIKeysColumn), fks...)) + })) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + fk := n.ProjectID + node, ok := nodeids[fk] + if !ok { + return fmt.Errorf(`unexpected referenced foreign-key "project_id" returned %v for node %v`, fk, n.ID) + } + assign(node, n) + } + return nil +} func (_q *ProjectQuery) loadDefaultAgentProvider(ctx context.Context, query *AgentProviderQuery, nodes []*Project, init func(*Project), assign func(*Project, *AgentProvider)) error { ids := make([]uuid.UUID, 0, len(nodes)) nodeids := make(map[uuid.UUID][]*Project) diff --git a/ent/project_update.go b/ent/project_update.go index bff57a5dc..1a1316b9b 100644 --- a/ent/project_update.go +++ b/ent/project_update.go @@ -32,6 +32,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/skill" "github.com/BetterAndBetterII/openase/ent/ticket" "github.com/BetterAndBetterII/openase/ent/ticketstatus" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/workflow" "github.com/BetterAndBetterII/openase/internal/domain/githubauth" "github.com/google/uuid" @@ -566,6 +567,21 @@ func (_u *ProjectUpdate) AddNotificationRules(v ...*NotificationRule) *ProjectUp return _u.AddNotificationRuleIDs(ids...) } +// AddUserAPIKeyIDs adds the "user_api_keys" edge to the UserAPIKey entity by IDs. +func (_u *ProjectUpdate) AddUserAPIKeyIDs(ids ...uuid.UUID) *ProjectUpdate { + _u.mutation.AddUserAPIKeyIDs(ids...) + return _u +} + +// AddUserAPIKeys adds the "user_api_keys" edges to the UserAPIKey entity. +func (_u *ProjectUpdate) AddUserAPIKeys(v ...*UserAPIKey) *ProjectUpdate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.AddUserAPIKeyIDs(ids...) +} + // SetDefaultAgentProvider sets the "default_agent_provider" edge to the AgentProvider entity. func (_u *ProjectUpdate) SetDefaultAgentProvider(v *AgentProvider) *ProjectUpdate { return _u.SetDefaultAgentProviderID(v.ID) @@ -960,6 +976,27 @@ func (_u *ProjectUpdate) RemoveNotificationRules(v ...*NotificationRule) *Projec return _u.RemoveNotificationRuleIDs(ids...) } +// ClearUserAPIKeys clears all "user_api_keys" edges to the UserAPIKey entity. +func (_u *ProjectUpdate) ClearUserAPIKeys() *ProjectUpdate { + _u.mutation.ClearUserAPIKeys() + return _u +} + +// RemoveUserAPIKeyIDs removes the "user_api_keys" edge to UserAPIKey entities by IDs. +func (_u *ProjectUpdate) RemoveUserAPIKeyIDs(ids ...uuid.UUID) *ProjectUpdate { + _u.mutation.RemoveUserAPIKeyIDs(ids...) + return _u +} + +// RemoveUserAPIKeys removes "user_api_keys" edges to UserAPIKey entities. +func (_u *ProjectUpdate) RemoveUserAPIKeys(v ...*UserAPIKey) *ProjectUpdate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.RemoveUserAPIKeyIDs(ids...) +} + // ClearDefaultAgentProvider clears the "default_agent_provider" edge to the AgentProvider entity. func (_u *ProjectUpdate) ClearDefaultAgentProvider() *ProjectUpdate { _u.mutation.ClearDefaultAgentProvider() @@ -1932,6 +1969,51 @@ func (_u *ProjectUpdate) sqlSave(ctx context.Context) (_node int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.UserAPIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.RemovedUserAPIKeysIDs(); len(nodes) > 0 && !_u.mutation.UserAPIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.UserAPIKeysIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if _u.mutation.DefaultAgentProviderCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -2497,6 +2579,21 @@ func (_u *ProjectUpdateOne) AddNotificationRules(v ...*NotificationRule) *Projec return _u.AddNotificationRuleIDs(ids...) } +// AddUserAPIKeyIDs adds the "user_api_keys" edge to the UserAPIKey entity by IDs. +func (_u *ProjectUpdateOne) AddUserAPIKeyIDs(ids ...uuid.UUID) *ProjectUpdateOne { + _u.mutation.AddUserAPIKeyIDs(ids...) + return _u +} + +// AddUserAPIKeys adds the "user_api_keys" edges to the UserAPIKey entity. +func (_u *ProjectUpdateOne) AddUserAPIKeys(v ...*UserAPIKey) *ProjectUpdateOne { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.AddUserAPIKeyIDs(ids...) +} + // SetDefaultAgentProvider sets the "default_agent_provider" edge to the AgentProvider entity. func (_u *ProjectUpdateOne) SetDefaultAgentProvider(v *AgentProvider) *ProjectUpdateOne { return _u.SetDefaultAgentProviderID(v.ID) @@ -2891,6 +2988,27 @@ func (_u *ProjectUpdateOne) RemoveNotificationRules(v ...*NotificationRule) *Pro return _u.RemoveNotificationRuleIDs(ids...) } +// ClearUserAPIKeys clears all "user_api_keys" edges to the UserAPIKey entity. +func (_u *ProjectUpdateOne) ClearUserAPIKeys() *ProjectUpdateOne { + _u.mutation.ClearUserAPIKeys() + return _u +} + +// RemoveUserAPIKeyIDs removes the "user_api_keys" edge to UserAPIKey entities by IDs. +func (_u *ProjectUpdateOne) RemoveUserAPIKeyIDs(ids ...uuid.UUID) *ProjectUpdateOne { + _u.mutation.RemoveUserAPIKeyIDs(ids...) + return _u +} + +// RemoveUserAPIKeys removes "user_api_keys" edges to UserAPIKey entities. +func (_u *ProjectUpdateOne) RemoveUserAPIKeys(v ...*UserAPIKey) *ProjectUpdateOne { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.RemoveUserAPIKeyIDs(ids...) +} + // ClearDefaultAgentProvider clears the "default_agent_provider" edge to the AgentProvider entity. func (_u *ProjectUpdateOne) ClearDefaultAgentProvider() *ProjectUpdateOne { _u.mutation.ClearDefaultAgentProvider() @@ -3893,6 +4011,51 @@ func (_u *ProjectUpdateOne) sqlSave(ctx context.Context) (_node *Project, err er } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.UserAPIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.RemovedUserAPIKeysIDs(); len(nodes) > 0 && !_u.mutation.UserAPIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.UserAPIKeysIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: project.UserAPIKeysTable, + Columns: []string{project.UserAPIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if _u.mutation.DefaultAgentProviderCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/ent/runtime.go b/ent/runtime.go index c1cac151f..a65ec394a 100644 --- a/ent/runtime.go +++ b/ent/runtime.go @@ -61,6 +61,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/ticketrepoworkspace" "github.com/BetterAndBetterII/openase/ent/ticketstatus" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/BetterAndBetterII/openase/ent/usergroupmembership" "github.com/BetterAndBetterII/openase/ent/useridentity" "github.com/BetterAndBetterII/openase/ent/workflow" @@ -1699,6 +1700,42 @@ func init() { userDescID := userFields[0].Descriptor() // user.DefaultID holds the default value on creation for the id field. user.DefaultID = userDescID.Default.(func() uuid.UUID) + userapikeyFields := schema.UserAPIKey{}.Fields() + _ = userapikeyFields + // userapikeyDescName is the schema descriptor for name field. + userapikeyDescName := userapikeyFields[3].Descriptor() + // userapikey.NameValidator is a validator for the "name" field. It is called by the builders before save. + userapikey.NameValidator = userapikeyDescName.Validators[0].(func(string) error) + // userapikeyDescTokenPrefix is the schema descriptor for token_prefix field. + userapikeyDescTokenPrefix := userapikeyFields[4].Descriptor() + // userapikey.TokenPrefixValidator is a validator for the "token_prefix" field. It is called by the builders before save. + userapikey.TokenPrefixValidator = userapikeyDescTokenPrefix.Validators[0].(func(string) error) + // userapikeyDescTokenHint is the schema descriptor for token_hint field. + userapikeyDescTokenHint := userapikeyFields[5].Descriptor() + // userapikey.TokenHintValidator is a validator for the "token_hint" field. It is called by the builders before save. + userapikey.TokenHintValidator = userapikeyDescTokenHint.Validators[0].(func(string) error) + // userapikeyDescTokenHash is the schema descriptor for token_hash field. + userapikeyDescTokenHash := userapikeyFields[6].Descriptor() + // userapikey.TokenHashValidator is a validator for the "token_hash" field. It is called by the builders before save. + userapikey.TokenHashValidator = userapikeyDescTokenHash.Validators[0].(func(string) error) + // userapikeyDescScopes is the schema descriptor for scopes field. + userapikeyDescScopes := userapikeyFields[7].Descriptor() + // userapikey.DefaultScopes holds the default value on creation for the scopes field. + userapikey.DefaultScopes = userapikeyDescScopes.Default.([]string) + // userapikeyDescCreatedAt is the schema descriptor for created_at field. + userapikeyDescCreatedAt := userapikeyFields[11].Descriptor() + // userapikey.DefaultCreatedAt holds the default value on creation for the created_at field. + userapikey.DefaultCreatedAt = userapikeyDescCreatedAt.Default.(func() time.Time) + // userapikeyDescUpdatedAt is the schema descriptor for updated_at field. + userapikeyDescUpdatedAt := userapikeyFields[12].Descriptor() + // userapikey.DefaultUpdatedAt holds the default value on creation for the updated_at field. + userapikey.DefaultUpdatedAt = userapikeyDescUpdatedAt.Default.(func() time.Time) + // userapikey.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. + userapikey.UpdateDefaultUpdatedAt = userapikeyDescUpdatedAt.UpdateDefault.(func() time.Time) + // userapikeyDescID is the schema descriptor for id field. + userapikeyDescID := userapikeyFields[0].Descriptor() + // userapikey.DefaultID holds the default value on creation for the id field. + userapikey.DefaultID = userapikeyDescID.Default.(func() uuid.UUID) usergroupmembershipFields := schema.UserGroupMembership{}.Fields() _ = usergroupmembershipFields // usergroupmembershipDescIssuer is the schema descriptor for issuer field. diff --git a/ent/schema/project.go b/ent/schema/project.go index dec9a52e8..42cb40754 100644 --- a/ent/schema/project.go +++ b/ent/schema/project.go @@ -76,6 +76,7 @@ func (Project) Edges() []ent.Edge { edge.To("update_threads", ProjectUpdateThread.Type), edge.To("chat_conversations", ChatConversation.Type), edge.To("notification_rules", NotificationRule.Type), + edge.To("user_api_keys", UserAPIKey.Type), edge.To("default_agent_provider", AgentProvider.Type). Field("default_agent_provider_id"). Unique(), diff --git a/ent/schema/user.go b/ent/schema/user.go index cb099f276..8d3940c94 100644 --- a/ent/schema/user.go +++ b/ent/schema/user.go @@ -30,6 +30,7 @@ func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("organization_memberships", OrganizationMembership.Type), edge.To("accepted_organization_invitations", OrganizationInvitation.Type), + edge.To("api_keys", UserAPIKey.Type), } } diff --git a/ent/schema/userapikey.go b/ent/schema/userapikey.go new file mode 100644 index 000000000..1ec7f5cb8 --- /dev/null +++ b/ent/schema/userapikey.go @@ -0,0 +1,58 @@ +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/schema/edge" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" +) + +type UserAPIKey struct { + ent.Schema +} + +func (UserAPIKey) Fields() []ent.Field { + return []ent.Field{ + uuidField(), + field.UUID("user_id", uuidZero()), + field.UUID("project_id", uuidZero()), + field.String("name").NotEmpty(), + field.String("token_prefix").NotEmpty(), + field.String("token_hint").NotEmpty(), + field.String("token_hash").NotEmpty(), + field.JSON("scopes", []string{}).Default([]string{}), + field.Enum("status").Values("active", "disabled", "revoked").Default("active"), + field.Time("expires_at").Optional().Nillable(), + field.Time("last_used_at").Optional().Nillable(), + createdAtField(), + field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now), + field.Time("disabled_at").Optional().Nillable(), + field.Time("revoked_at").Optional().Nillable(), + } +} + +func (UserAPIKey) Edges() []ent.Edge { + return []ent.Edge{ + edge.From("user", User.Type). + Ref("api_keys"). + Field("user_id"). + Unique(). + Required(), + edge.From("project", Project.Type). + Ref("user_api_keys"). + Field("project_id"). + Unique(). + Required(), + } +} + +func (UserAPIKey) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("token_hash").Unique(), + index.Fields("project_id", "user_id"), + index.Fields("project_id", "status"), + index.Fields("expires_at"), + } +} diff --git a/ent/tx.go b/ent/tx.go index 5eedf4251..c0e78a9c2 100644 --- a/ent/tx.go +++ b/ent/tx.go @@ -122,6 +122,8 @@ type Tx struct { TicketStatus *TicketStatusClient // User is the client for interacting with the User builders. User *UserClient + // UserAPIKey is the client for interacting with the UserAPIKey builders. + UserAPIKey *UserAPIKeyClient // UserGroupMembership is the client for interacting with the UserGroupMembership builders. UserGroupMembership *UserGroupMembershipClient // UserIdentity is the client for interacting with the UserIdentity builders. @@ -320,6 +322,7 @@ func (tx *Tx) init() { tx.TicketRepoWorkspace = NewTicketRepoWorkspaceClient(tx.config) tx.TicketStatus = NewTicketStatusClient(tx.config) tx.User = NewUserClient(tx.config) + tx.UserAPIKey = NewUserAPIKeyClient(tx.config) tx.UserGroupMembership = NewUserGroupMembershipClient(tx.config) tx.UserIdentity = NewUserIdentityClient(tx.config) tx.Workflow = NewWorkflowClient(tx.config) diff --git a/ent/user.go b/ent/user.go index 6326eb80e..4bd296baa 100644 --- a/ent/user.go +++ b/ent/user.go @@ -44,9 +44,11 @@ type UserEdges struct { OrganizationMemberships []*OrganizationMembership `json:"organization_memberships,omitempty"` // AcceptedOrganizationInvitations holds the value of the accepted_organization_invitations edge. AcceptedOrganizationInvitations []*OrganizationInvitation `json:"accepted_organization_invitations,omitempty"` + // APIKeys holds the value of the api_keys edge. + APIKeys []*UserAPIKey `json:"api_keys,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [2]bool + loadedTypes [3]bool } // OrganizationMembershipsOrErr returns the OrganizationMemberships value or an error if the edge @@ -67,6 +69,15 @@ func (e UserEdges) AcceptedOrganizationInvitationsOrErr() ([]*OrganizationInvita return nil, &NotLoadedError{edge: "accepted_organization_invitations"} } +// APIKeysOrErr returns the APIKeys value or an error if the edge +// was not loaded in eager-loading. +func (e UserEdges) APIKeysOrErr() ([]*UserAPIKey, error) { + if e.loadedTypes[2] { + return e.APIKeys, nil + } + return nil, &NotLoadedError{edge: "api_keys"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*User) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) @@ -165,6 +176,11 @@ func (_m *User) QueryAcceptedOrganizationInvitations() *OrganizationInvitationQu return NewUserClient(_m.config).QueryAcceptedOrganizationInvitations(_m) } +// QueryAPIKeys queries the "api_keys" edge of the User entity. +func (_m *User) QueryAPIKeys() *UserAPIKeyQuery { + return NewUserClient(_m.config).QueryAPIKeys(_m) +} + // Update returns a builder for updating this User. // Note that you need to call User.Unwrap() before calling this method if this User // was returned from a transaction, and the transaction was committed or rolled back. diff --git a/ent/user/user.go b/ent/user/user.go index 2a898e11a..b42637d3d 100644 --- a/ent/user/user.go +++ b/ent/user/user.go @@ -34,6 +34,8 @@ const ( EdgeOrganizationMemberships = "organization_memberships" // EdgeAcceptedOrganizationInvitations holds the string denoting the accepted_organization_invitations edge name in mutations. EdgeAcceptedOrganizationInvitations = "accepted_organization_invitations" + // EdgeAPIKeys holds the string denoting the api_keys edge name in mutations. + EdgeAPIKeys = "api_keys" // Table holds the table name of the user in the database. Table = "users" // OrganizationMembershipsTable is the table that holds the organization_memberships relation/edge. @@ -50,6 +52,13 @@ const ( AcceptedOrganizationInvitationsInverseTable = "organization_invitations" // AcceptedOrganizationInvitationsColumn is the table column denoting the accepted_organization_invitations relation/edge. AcceptedOrganizationInvitationsColumn = "accepted_by_user_id" + // APIKeysTable is the table that holds the api_keys relation/edge. + APIKeysTable = "user_api_keys" + // APIKeysInverseTable is the table name for the UserAPIKey entity. + // It exists in this package in order to avoid circular dependency with the "userapikey" package. + APIKeysInverseTable = "user_api_keys" + // APIKeysColumn is the table column denoting the api_keys relation/edge. + APIKeysColumn = "user_id" ) // Columns holds all SQL columns for user fields. @@ -187,6 +196,20 @@ func ByAcceptedOrganizationInvitations(term sql.OrderTerm, terms ...sql.OrderTer sqlgraph.OrderByNeighborTerms(s, newAcceptedOrganizationInvitationsStep(), append([]sql.OrderTerm{term}, terms...)...) } } + +// ByAPIKeysCount orders the results by api_keys count. +func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newAPIKeysStep(), opts...) + } +} + +// ByAPIKeys orders the results by api_keys terms. +func ByAPIKeys(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newAPIKeysStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} func newOrganizationMembershipsStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), @@ -201,3 +224,10 @@ func newAcceptedOrganizationInvitationsStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.O2M, false, AcceptedOrganizationInvitationsTable, AcceptedOrganizationInvitationsColumn), ) } +func newAPIKeysStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(APIKeysInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, APIKeysTable, APIKeysColumn), + ) +} diff --git a/ent/user/where.go b/ent/user/where.go index 913cf6250..f18b0625f 100644 --- a/ent/user/where.go +++ b/ent/user/where.go @@ -477,6 +477,29 @@ func HasAcceptedOrganizationInvitationsWith(preds ...predicate.OrganizationInvit }) } +// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge. +func HasAPIKeys() predicate.User { + return predicate.User(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, APIKeysTable, APIKeysColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasAPIKeysWith applies the HasEdge predicate on the "api_keys" edge with a given conditions (other predicates). +func HasAPIKeysWith(preds ...predicate.UserAPIKey) predicate.User { + return predicate.User(func(s *sql.Selector) { + step := newAPIKeysStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.User) predicate.User { return predicate.User(sql.AndPredicates(predicates...)) diff --git a/ent/user_create.go b/ent/user_create.go index 28c070206..7affbd852 100644 --- a/ent/user_create.go +++ b/ent/user_create.go @@ -13,6 +13,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/organizationinvitation" "github.com/BetterAndBetterII/openase/ent/organizationmembership" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/google/uuid" ) @@ -165,6 +166,21 @@ func (_c *UserCreate) AddAcceptedOrganizationInvitations(v ...*OrganizationInvit return _c.AddAcceptedOrganizationInvitationIDs(ids...) } +// AddAPIKeyIDs adds the "api_keys" edge to the UserAPIKey entity by IDs. +func (_c *UserCreate) AddAPIKeyIDs(ids ...uuid.UUID) *UserCreate { + _c.mutation.AddAPIKeyIDs(ids...) + return _c +} + +// AddAPIKeys adds the "api_keys" edges to the UserAPIKey entity. +func (_c *UserCreate) AddAPIKeys(v ...*UserAPIKey) *UserCreate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _c.AddAPIKeyIDs(ids...) +} + // Mutation returns the UserMutation object of the builder. func (_c *UserCreate) Mutation() *UserMutation { return _c.mutation @@ -350,6 +366,22 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { } _spec.Edges = append(_spec.Edges, edge) } + if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } diff --git a/ent/user_query.go b/ent/user_query.go index 4f5a239f8..f46b47f4b 100644 --- a/ent/user_query.go +++ b/ent/user_query.go @@ -16,6 +16,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/organizationmembership" "github.com/BetterAndBetterII/openase/ent/predicate" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/google/uuid" ) @@ -28,6 +29,7 @@ type UserQuery struct { predicates []predicate.User withOrganizationMemberships *OrganizationMembershipQuery withAcceptedOrganizationInvitations *OrganizationInvitationQuery + withAPIKeys *UserAPIKeyQuery // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -108,6 +110,28 @@ func (_q *UserQuery) QueryAcceptedOrganizationInvitations() *OrganizationInvitat return query } +// QueryAPIKeys chains the current query on the "api_keys" edge. +func (_q *UserQuery) QueryAPIKeys() *UserAPIKeyQuery { + query := (&UserAPIKeyClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(user.Table, user.FieldID, selector), + sqlgraph.To(userapikey.Table, userapikey.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, user.APIKeysTable, user.APIKeysColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first User entity from the query. // Returns a *NotFoundError when no User was found. func (_q *UserQuery) First(ctx context.Context) (*User, error) { @@ -302,6 +326,7 @@ func (_q *UserQuery) Clone() *UserQuery { predicates: append([]predicate.User{}, _q.predicates...), withOrganizationMemberships: _q.withOrganizationMemberships.Clone(), withAcceptedOrganizationInvitations: _q.withAcceptedOrganizationInvitations.Clone(), + withAPIKeys: _q.withAPIKeys.Clone(), // clone intermediate query. sql: _q.sql.Clone(), path: _q.path, @@ -330,6 +355,17 @@ func (_q *UserQuery) WithAcceptedOrganizationInvitations(opts ...func(*Organizat return _q } +// WithAPIKeys tells the query-builder to eager-load the nodes that are connected to +// the "api_keys" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *UserQuery) WithAPIKeys(opts ...func(*UserAPIKeyQuery)) *UserQuery { + query := (&UserAPIKeyClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withAPIKeys = query + return _q +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -408,9 +444,10 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e var ( nodes = []*User{} _spec = _q.querySpec() - loadedTypes = [2]bool{ + loadedTypes = [3]bool{ _q.withOrganizationMemberships != nil, _q.withAcceptedOrganizationInvitations != nil, + _q.withAPIKeys != nil, } ) _spec.ScanValues = func(columns []string) ([]any, error) { @@ -449,6 +486,13 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e return nil, err } } + if query := _q.withAPIKeys; query != nil { + if err := _q.loadAPIKeys(ctx, query, nodes, + func(n *User) { n.Edges.APIKeys = []*UserAPIKey{} }, + func(n *User, e *UserAPIKey) { n.Edges.APIKeys = append(n.Edges.APIKeys, e) }); err != nil { + return nil, err + } + } return nodes, nil } @@ -518,6 +562,36 @@ func (_q *UserQuery) loadAcceptedOrganizationInvitations(ctx context.Context, qu } return nil } +func (_q *UserQuery) loadAPIKeys(ctx context.Context, query *UserAPIKeyQuery, nodes []*User, init func(*User), assign func(*User, *UserAPIKey)) error { + fks := make([]driver.Value, 0, len(nodes)) + nodeids := make(map[uuid.UUID]*User) + for i := range nodes { + fks = append(fks, nodes[i].ID) + nodeids[nodes[i].ID] = nodes[i] + if init != nil { + init(nodes[i]) + } + } + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(userapikey.FieldUserID) + } + query.Where(predicate.UserAPIKey(func(s *sql.Selector) { + s.Where(sql.InValues(s.C(user.APIKeysColumn), fks...)) + })) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + fk := n.UserID + node, ok := nodeids[fk] + if !ok { + return fmt.Errorf(`unexpected referenced foreign-key "user_id" returned %v for node %v`, fk, n.ID) + } + assign(node, n) + } + return nil +} func (_q *UserQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() diff --git a/ent/user_update.go b/ent/user_update.go index dd798766b..538d6a385 100644 --- a/ent/user_update.go +++ b/ent/user_update.go @@ -15,6 +15,7 @@ import ( "github.com/BetterAndBetterII/openase/ent/organizationmembership" "github.com/BetterAndBetterII/openase/ent/predicate" "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" "github.com/google/uuid" ) @@ -143,6 +144,21 @@ func (_u *UserUpdate) AddAcceptedOrganizationInvitations(v ...*OrganizationInvit return _u.AddAcceptedOrganizationInvitationIDs(ids...) } +// AddAPIKeyIDs adds the "api_keys" edge to the UserAPIKey entity by IDs. +func (_u *UserUpdate) AddAPIKeyIDs(ids ...uuid.UUID) *UserUpdate { + _u.mutation.AddAPIKeyIDs(ids...) + return _u +} + +// AddAPIKeys adds the "api_keys" edges to the UserAPIKey entity. +func (_u *UserUpdate) AddAPIKeys(v ...*UserAPIKey) *UserUpdate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.AddAPIKeyIDs(ids...) +} + // Mutation returns the UserMutation object of the builder. func (_u *UserUpdate) Mutation() *UserMutation { return _u.mutation @@ -190,6 +206,27 @@ func (_u *UserUpdate) RemoveAcceptedOrganizationInvitations(v ...*OrganizationIn return _u.RemoveAcceptedOrganizationInvitationIDs(ids...) } +// ClearAPIKeys clears all "api_keys" edges to the UserAPIKey entity. +func (_u *UserUpdate) ClearAPIKeys() *UserUpdate { + _u.mutation.ClearAPIKeys() + return _u +} + +// RemoveAPIKeyIDs removes the "api_keys" edge to UserAPIKey entities by IDs. +func (_u *UserUpdate) RemoveAPIKeyIDs(ids ...uuid.UUID) *UserUpdate { + _u.mutation.RemoveAPIKeyIDs(ids...) + return _u +} + +// RemoveAPIKeys removes "api_keys" edges to UserAPIKey entities. +func (_u *UserUpdate) RemoveAPIKeys(v ...*UserAPIKey) *UserUpdate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.RemoveAPIKeyIDs(ids...) +} + // Save executes the query and returns the number of nodes affected by the update operation. func (_u *UserUpdate) Save(ctx context.Context) (int, error) { _u.defaults() @@ -359,6 +396,51 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.APIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.RemovedAPIKeysIDs(); len(nodes) > 0 && !_u.mutation.APIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.APIKeysIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{user.Label} @@ -491,6 +573,21 @@ func (_u *UserUpdateOne) AddAcceptedOrganizationInvitations(v ...*OrganizationIn return _u.AddAcceptedOrganizationInvitationIDs(ids...) } +// AddAPIKeyIDs adds the "api_keys" edge to the UserAPIKey entity by IDs. +func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...uuid.UUID) *UserUpdateOne { + _u.mutation.AddAPIKeyIDs(ids...) + return _u +} + +// AddAPIKeys adds the "api_keys" edges to the UserAPIKey entity. +func (_u *UserUpdateOne) AddAPIKeys(v ...*UserAPIKey) *UserUpdateOne { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.AddAPIKeyIDs(ids...) +} + // Mutation returns the UserMutation object of the builder. func (_u *UserUpdateOne) Mutation() *UserMutation { return _u.mutation @@ -538,6 +635,27 @@ func (_u *UserUpdateOne) RemoveAcceptedOrganizationInvitations(v ...*Organizatio return _u.RemoveAcceptedOrganizationInvitationIDs(ids...) } +// ClearAPIKeys clears all "api_keys" edges to the UserAPIKey entity. +func (_u *UserUpdateOne) ClearAPIKeys() *UserUpdateOne { + _u.mutation.ClearAPIKeys() + return _u +} + +// RemoveAPIKeyIDs removes the "api_keys" edge to UserAPIKey entities by IDs. +func (_u *UserUpdateOne) RemoveAPIKeyIDs(ids ...uuid.UUID) *UserUpdateOne { + _u.mutation.RemoveAPIKeyIDs(ids...) + return _u +} + +// RemoveAPIKeys removes "api_keys" edges to UserAPIKey entities. +func (_u *UserUpdateOne) RemoveAPIKeys(v ...*UserAPIKey) *UserUpdateOne { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.RemoveAPIKeyIDs(ids...) +} + // Where appends a list predicates to the UserUpdate builder. func (_u *UserUpdateOne) Where(ps ...predicate.User) *UserUpdateOne { _u.mutation.Where(ps...) @@ -737,6 +855,51 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.APIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.RemovedAPIKeysIDs(); len(nodes) > 0 && !_u.mutation.APIKeysCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.APIKeysIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.APIKeysTable, + Columns: []string{user.APIKeysColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _node = &User{config: _u.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/ent/userapikey.go b/ent/userapikey.go new file mode 100644 index 000000000..620b3431f --- /dev/null +++ b/ent/userapikey.go @@ -0,0 +1,315 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "github.com/BetterAndBetterII/openase/ent/project" + "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" + "github.com/google/uuid" +) + +// UserAPIKey is the model entity for the UserAPIKey schema. +type UserAPIKey struct { + config `json:"-"` + // ID of the ent. + ID uuid.UUID `json:"id,omitempty"` + // UserID holds the value of the "user_id" field. + UserID uuid.UUID `json:"user_id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` + // Name holds the value of the "name" field. + Name string `json:"name,omitempty"` + // TokenPrefix holds the value of the "token_prefix" field. + TokenPrefix string `json:"token_prefix,omitempty"` + // TokenHint holds the value of the "token_hint" field. + TokenHint string `json:"token_hint,omitempty"` + // TokenHash holds the value of the "token_hash" field. + TokenHash string `json:"token_hash,omitempty"` + // Scopes holds the value of the "scopes" field. + Scopes []string `json:"scopes,omitempty"` + // Status holds the value of the "status" field. + Status userapikey.Status `json:"status,omitempty"` + // ExpiresAt holds the value of the "expires_at" field. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // LastUsedAt holds the value of the "last_used_at" field. + LastUsedAt *time.Time `json:"last_used_at,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` + // UpdatedAt holds the value of the "updated_at" field. + UpdatedAt time.Time `json:"updated_at,omitempty"` + // DisabledAt holds the value of the "disabled_at" field. + DisabledAt *time.Time `json:"disabled_at,omitempty"` + // RevokedAt holds the value of the "revoked_at" field. + RevokedAt *time.Time `json:"revoked_at,omitempty"` + // Edges holds the relations/edges for other nodes in the graph. + // The values are being populated by the UserAPIKeyQuery when eager-loading is set. + Edges UserAPIKeyEdges `json:"edges"` + selectValues sql.SelectValues +} + +// UserAPIKeyEdges holds the relations/edges for other nodes in the graph. +type UserAPIKeyEdges struct { + // User holds the value of the user edge. + User *User `json:"user,omitempty"` + // Project holds the value of the project edge. + Project *Project `json:"project,omitempty"` + // loadedTypes holds the information for reporting if a + // type was loaded (or requested) in eager-loading or not. + loadedTypes [2]bool +} + +// UserOrErr returns the User value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e UserAPIKeyEdges) UserOrErr() (*User, error) { + if e.User != nil { + return e.User, nil + } else if e.loadedTypes[0] { + return nil, &NotFoundError{label: user.Label} + } + return nil, &NotLoadedError{edge: "user"} +} + +// ProjectOrErr returns the Project value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e UserAPIKeyEdges) ProjectOrErr() (*Project, error) { + if e.Project != nil { + return e.Project, nil + } else if e.loadedTypes[1] { + return nil, &NotFoundError{label: project.Label} + } + return nil, &NotLoadedError{edge: "project"} +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*UserAPIKey) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case userapikey.FieldScopes: + values[i] = new([]byte) + case userapikey.FieldName, userapikey.FieldTokenPrefix, userapikey.FieldTokenHint, userapikey.FieldTokenHash, userapikey.FieldStatus: + values[i] = new(sql.NullString) + case userapikey.FieldExpiresAt, userapikey.FieldLastUsedAt, userapikey.FieldCreatedAt, userapikey.FieldUpdatedAt, userapikey.FieldDisabledAt, userapikey.FieldRevokedAt: + values[i] = new(sql.NullTime) + case userapikey.FieldID, userapikey.FieldUserID, userapikey.FieldProjectID: + values[i] = new(uuid.UUID) + default: + values[i] = new(sql.UnknownType) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the UserAPIKey fields. +func (_m *UserAPIKey) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case userapikey.FieldID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field id", values[i]) + } else if value != nil { + _m.ID = *value + } + case userapikey.FieldUserID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field user_id", values[i]) + } else if value != nil { + _m.UserID = *value + } + case userapikey.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + _m.ProjectID = *value + } + case userapikey.FieldName: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field name", values[i]) + } else if value.Valid { + _m.Name = value.String + } + case userapikey.FieldTokenPrefix: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field token_prefix", values[i]) + } else if value.Valid { + _m.TokenPrefix = value.String + } + case userapikey.FieldTokenHint: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field token_hint", values[i]) + } else if value.Valid { + _m.TokenHint = value.String + } + case userapikey.FieldTokenHash: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field token_hash", values[i]) + } else if value.Valid { + _m.TokenHash = value.String + } + case userapikey.FieldScopes: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field scopes", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.Scopes); err != nil { + return fmt.Errorf("unmarshal field scopes: %w", err) + } + } + case userapikey.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + _m.Status = userapikey.Status(value.String) + } + case userapikey.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = new(time.Time) + *_m.ExpiresAt = value.Time + } + case userapikey.FieldLastUsedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_used_at", values[i]) + } else if value.Valid { + _m.LastUsedAt = new(time.Time) + *_m.LastUsedAt = value.Time + } + case userapikey.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field created_at", values[i]) + } else if value.Valid { + _m.CreatedAt = value.Time + } + case userapikey.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updated_at", values[i]) + } else if value.Valid { + _m.UpdatedAt = value.Time + } + case userapikey.FieldDisabledAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field disabled_at", values[i]) + } else if value.Valid { + _m.DisabledAt = new(time.Time) + *_m.DisabledAt = value.Time + } + case userapikey.FieldRevokedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field revoked_at", values[i]) + } else if value.Valid { + _m.RevokedAt = new(time.Time) + *_m.RevokedAt = value.Time + } + default: + _m.selectValues.Set(columns[i], values[i]) + } + } + return nil +} + +// Value returns the ent.Value that was dynamically selected and assigned to the UserAPIKey. +// This includes values selected through modifiers, order, etc. +func (_m *UserAPIKey) Value(name string) (ent.Value, error) { + return _m.selectValues.Get(name) +} + +// QueryUser queries the "user" edge of the UserAPIKey entity. +func (_m *UserAPIKey) QueryUser() *UserQuery { + return NewUserAPIKeyClient(_m.config).QueryUser(_m) +} + +// QueryProject queries the "project" edge of the UserAPIKey entity. +func (_m *UserAPIKey) QueryProject() *ProjectQuery { + return NewUserAPIKeyClient(_m.config).QueryProject(_m) +} + +// Update returns a builder for updating this UserAPIKey. +// Note that you need to call UserAPIKey.Unwrap() before calling this method if this UserAPIKey +// was returned from a transaction, and the transaction was committed or rolled back. +func (_m *UserAPIKey) Update() *UserAPIKeyUpdateOne { + return NewUserAPIKeyClient(_m.config).UpdateOne(_m) +} + +// Unwrap unwraps the UserAPIKey entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (_m *UserAPIKey) Unwrap() *UserAPIKey { + _tx, ok := _m.config.driver.(*txDriver) + if !ok { + panic("ent: UserAPIKey is not a transactional entity") + } + _m.config.driver = _tx.drv + return _m +} + +// String implements the fmt.Stringer. +func (_m *UserAPIKey) String() string { + var builder strings.Builder + builder.WriteString("UserAPIKey(") + builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("user_id=") + builder.WriteString(fmt.Sprintf("%v", _m.UserID)) + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", _m.ProjectID)) + builder.WriteString(", ") + builder.WriteString("name=") + builder.WriteString(_m.Name) + builder.WriteString(", ") + builder.WriteString("token_prefix=") + builder.WriteString(_m.TokenPrefix) + builder.WriteString(", ") + builder.WriteString("token_hint=") + builder.WriteString(_m.TokenHint) + builder.WriteString(", ") + builder.WriteString("token_hash=") + builder.WriteString(_m.TokenHash) + builder.WriteString(", ") + builder.WriteString("scopes=") + builder.WriteString(fmt.Sprintf("%v", _m.Scopes)) + builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(fmt.Sprintf("%v", _m.Status)) + builder.WriteString(", ") + if v := _m.ExpiresAt; v != nil { + builder.WriteString("expires_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.LastUsedAt; v != nil { + builder.WriteString("last_used_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("created_at=") + builder.WriteString(_m.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updated_at=") + builder.WriteString(_m.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + if v := _m.DisabledAt; v != nil { + builder.WriteString("disabled_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + if v := _m.RevokedAt; v != nil { + builder.WriteString("revoked_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteByte(')') + return builder.String() +} + +// UserAPIKeys is a parsable slice of UserAPIKey. +type UserAPIKeys []*UserAPIKey diff --git a/ent/userapikey/userapikey.go b/ent/userapikey/userapikey.go new file mode 100644 index 000000000..bd29ead23 --- /dev/null +++ b/ent/userapikey/userapikey.go @@ -0,0 +1,245 @@ +// Code generated by ent, DO NOT EDIT. + +package userapikey + +import ( + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/google/uuid" +) + +const ( + // Label holds the string label denoting the userapikey type in the database. + Label = "user_api_key" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldUserID holds the string denoting the user_id field in the database. + FieldUserID = "user_id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" + // FieldName holds the string denoting the name field in the database. + FieldName = "name" + // FieldTokenPrefix holds the string denoting the token_prefix field in the database. + FieldTokenPrefix = "token_prefix" + // FieldTokenHint holds the string denoting the token_hint field in the database. + FieldTokenHint = "token_hint" + // FieldTokenHash holds the string denoting the token_hash field in the database. + FieldTokenHash = "token_hash" + // FieldScopes holds the string denoting the scopes field in the database. + FieldScopes = "scopes" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldLastUsedAt holds the string denoting the last_used_at field in the database. + FieldLastUsedAt = "last_used_at" + // FieldCreatedAt holds the string denoting the created_at field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updated_at field in the database. + FieldUpdatedAt = "updated_at" + // FieldDisabledAt holds the string denoting the disabled_at field in the database. + FieldDisabledAt = "disabled_at" + // FieldRevokedAt holds the string denoting the revoked_at field in the database. + FieldRevokedAt = "revoked_at" + // EdgeUser holds the string denoting the user edge name in mutations. + EdgeUser = "user" + // EdgeProject holds the string denoting the project edge name in mutations. + EdgeProject = "project" + // Table holds the table name of the userapikey in the database. + Table = "user_api_keys" + // UserTable is the table that holds the user relation/edge. + UserTable = "user_api_keys" + // UserInverseTable is the table name for the User entity. + // It exists in this package in order to avoid circular dependency with the "user" package. + UserInverseTable = "users" + // UserColumn is the table column denoting the user relation/edge. + UserColumn = "user_id" + // ProjectTable is the table that holds the project relation/edge. + ProjectTable = "user_api_keys" + // ProjectInverseTable is the table name for the Project entity. + // It exists in this package in order to avoid circular dependency with the "project" package. + ProjectInverseTable = "projects" + // ProjectColumn is the table column denoting the project relation/edge. + ProjectColumn = "project_id" +) + +// Columns holds all SQL columns for userapikey fields. +var Columns = []string{ + FieldID, + FieldUserID, + FieldProjectID, + FieldName, + FieldTokenPrefix, + FieldTokenHint, + FieldTokenHash, + FieldScopes, + FieldStatus, + FieldExpiresAt, + FieldLastUsedAt, + FieldCreatedAt, + FieldUpdatedAt, + FieldDisabledAt, + FieldRevokedAt, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // NameValidator is a validator for the "name" field. It is called by the builders before save. + NameValidator func(string) error + // TokenPrefixValidator is a validator for the "token_prefix" field. It is called by the builders before save. + TokenPrefixValidator func(string) error + // TokenHintValidator is a validator for the "token_hint" field. It is called by the builders before save. + TokenHintValidator func(string) error + // TokenHashValidator is a validator for the "token_hash" field. It is called by the builders before save. + TokenHashValidator func(string) error + // DefaultScopes holds the default value on creation for the "scopes" field. + DefaultScopes []string + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. + UpdateDefaultUpdatedAt func() time.Time + // DefaultID holds the default value on creation for the "id" field. + DefaultID func() uuid.UUID +) + +// Status defines the type for the "status" enum field. +type Status string + +// StatusActive is the default value of the Status enum. +const DefaultStatus = StatusActive + +// Status values. +const ( + StatusActive Status = "active" + StatusDisabled Status = "disabled" + StatusRevoked Status = "revoked" +) + +func (s Status) String() string { + return string(s) +} + +// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save. +func StatusValidator(s Status) error { + switch s { + case StatusActive, StatusDisabled, StatusRevoked: + return nil + default: + return fmt.Errorf("userapikey: invalid enum value for status field: %q", s) + } +} + +// OrderOption defines the ordering options for the UserAPIKey queries. +type OrderOption func(*sql.Selector) + +// ByID orders the results by the id field. +func ByID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldID, opts...).ToFunc() +} + +// ByUserID orders the results by the user_id field. +func ByUserID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUserID, opts...).ToFunc() +} + +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + +// ByName orders the results by the name field. +func ByName(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldName, opts...).ToFunc() +} + +// ByTokenPrefix orders the results by the token_prefix field. +func ByTokenPrefix(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTokenPrefix, opts...).ToFunc() +} + +// ByTokenHint orders the results by the token_hint field. +func ByTokenHint(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTokenHint, opts...).ToFunc() +} + +// ByTokenHash orders the results by the token_hash field. +func ByTokenHash(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTokenHash, opts...).ToFunc() +} + +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByLastUsedAt orders the results by the last_used_at field. +func ByLastUsedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc() +} + +// ByCreatedAt orders the results by the created_at field. +func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() +} + +// ByUpdatedAt orders the results by the updated_at field. +func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() +} + +// ByDisabledAt orders the results by the disabled_at field. +func ByDisabledAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldDisabledAt, opts...).ToFunc() +} + +// ByRevokedAt orders the results by the revoked_at field. +func ByRevokedAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRevokedAt, opts...).ToFunc() +} + +// ByUserField orders the results by user field. +func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...)) + } +} + +// ByProjectField orders the results by project field. +func ByProjectField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newProjectStep(), sql.OrderByField(field, opts...)) + } +} +func newUserStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(UserInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn), + ) +} +func newProjectStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(ProjectInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, ProjectTable, ProjectColumn), + ) +} diff --git a/ent/userapikey/where.go b/ent/userapikey/where.go new file mode 100644 index 000000000..5f534d8a3 --- /dev/null +++ b/ent/userapikey/where.go @@ -0,0 +1,778 @@ +// Code generated by ent, DO NOT EDIT. + +package userapikey + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/BetterAndBetterII/openase/ent/predicate" + "github.com/google/uuid" +) + +// ID filters vertices based on their ID field. +func ID(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldID, id)) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldID, id)) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldID, id)) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldID, ids...)) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldID, ids...)) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldID, id)) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldID, id)) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldID, id)) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldID, id)) +} + +// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ. +func UserID(v uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldUserID, v)) +} + +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldProjectID, v)) +} + +// Name applies equality check predicate on the "name" field. It's identical to NameEQ. +func Name(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldName, v)) +} + +// TokenPrefix applies equality check predicate on the "token_prefix" field. It's identical to TokenPrefixEQ. +func TokenPrefix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldTokenPrefix, v)) +} + +// TokenHint applies equality check predicate on the "token_hint" field. It's identical to TokenHintEQ. +func TokenHint(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldTokenHint, v)) +} + +// TokenHash applies equality check predicate on the "token_hash" field. It's identical to TokenHashEQ. +func TokenHash(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldTokenHash, v)) +} + +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldExpiresAt, v)) +} + +// LastUsedAt applies equality check predicate on the "last_used_at" field. It's identical to LastUsedAtEQ. +func LastUsedAt(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldLastUsedAt, v)) +} + +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldCreatedAt, v)) +} + +// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// DisabledAt applies equality check predicate on the "disabled_at" field. It's identical to DisabledAtEQ. +func DisabledAt(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldDisabledAt, v)) +} + +// RevokedAt applies equality check predicate on the "revoked_at" field. It's identical to RevokedAtEQ. +func RevokedAt(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldRevokedAt, v)) +} + +// UserIDEQ applies the EQ predicate on the "user_id" field. +func UserIDEQ(v uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldUserID, v)) +} + +// UserIDNEQ applies the NEQ predicate on the "user_id" field. +func UserIDNEQ(v uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldUserID, v)) +} + +// UserIDIn applies the In predicate on the "user_id" field. +func UserIDIn(vs ...uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldUserID, vs...)) +} + +// UserIDNotIn applies the NotIn predicate on the "user_id" field. +func UserIDNotIn(vs ...uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldUserID, vs...)) +} + +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// NameEQ applies the EQ predicate on the "name" field. +func NameEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldName, v)) +} + +// NameNEQ applies the NEQ predicate on the "name" field. +func NameNEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldName, v)) +} + +// NameIn applies the In predicate on the "name" field. +func NameIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldName, vs...)) +} + +// NameNotIn applies the NotIn predicate on the "name" field. +func NameNotIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldName, vs...)) +} + +// NameGT applies the GT predicate on the "name" field. +func NameGT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldName, v)) +} + +// NameGTE applies the GTE predicate on the "name" field. +func NameGTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldName, v)) +} + +// NameLT applies the LT predicate on the "name" field. +func NameLT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldName, v)) +} + +// NameLTE applies the LTE predicate on the "name" field. +func NameLTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldName, v)) +} + +// NameContains applies the Contains predicate on the "name" field. +func NameContains(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContains(FieldName, v)) +} + +// NameHasPrefix applies the HasPrefix predicate on the "name" field. +func NameHasPrefix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasPrefix(FieldName, v)) +} + +// NameHasSuffix applies the HasSuffix predicate on the "name" field. +func NameHasSuffix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasSuffix(FieldName, v)) +} + +// NameEqualFold applies the EqualFold predicate on the "name" field. +func NameEqualFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEqualFold(FieldName, v)) +} + +// NameContainsFold applies the ContainsFold predicate on the "name" field. +func NameContainsFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContainsFold(FieldName, v)) +} + +// TokenPrefixEQ applies the EQ predicate on the "token_prefix" field. +func TokenPrefixEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldTokenPrefix, v)) +} + +// TokenPrefixNEQ applies the NEQ predicate on the "token_prefix" field. +func TokenPrefixNEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldTokenPrefix, v)) +} + +// TokenPrefixIn applies the In predicate on the "token_prefix" field. +func TokenPrefixIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldTokenPrefix, vs...)) +} + +// TokenPrefixNotIn applies the NotIn predicate on the "token_prefix" field. +func TokenPrefixNotIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldTokenPrefix, vs...)) +} + +// TokenPrefixGT applies the GT predicate on the "token_prefix" field. +func TokenPrefixGT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldTokenPrefix, v)) +} + +// TokenPrefixGTE applies the GTE predicate on the "token_prefix" field. +func TokenPrefixGTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldTokenPrefix, v)) +} + +// TokenPrefixLT applies the LT predicate on the "token_prefix" field. +func TokenPrefixLT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldTokenPrefix, v)) +} + +// TokenPrefixLTE applies the LTE predicate on the "token_prefix" field. +func TokenPrefixLTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldTokenPrefix, v)) +} + +// TokenPrefixContains applies the Contains predicate on the "token_prefix" field. +func TokenPrefixContains(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContains(FieldTokenPrefix, v)) +} + +// TokenPrefixHasPrefix applies the HasPrefix predicate on the "token_prefix" field. +func TokenPrefixHasPrefix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasPrefix(FieldTokenPrefix, v)) +} + +// TokenPrefixHasSuffix applies the HasSuffix predicate on the "token_prefix" field. +func TokenPrefixHasSuffix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasSuffix(FieldTokenPrefix, v)) +} + +// TokenPrefixEqualFold applies the EqualFold predicate on the "token_prefix" field. +func TokenPrefixEqualFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEqualFold(FieldTokenPrefix, v)) +} + +// TokenPrefixContainsFold applies the ContainsFold predicate on the "token_prefix" field. +func TokenPrefixContainsFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContainsFold(FieldTokenPrefix, v)) +} + +// TokenHintEQ applies the EQ predicate on the "token_hint" field. +func TokenHintEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldTokenHint, v)) +} + +// TokenHintNEQ applies the NEQ predicate on the "token_hint" field. +func TokenHintNEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldTokenHint, v)) +} + +// TokenHintIn applies the In predicate on the "token_hint" field. +func TokenHintIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldTokenHint, vs...)) +} + +// TokenHintNotIn applies the NotIn predicate on the "token_hint" field. +func TokenHintNotIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldTokenHint, vs...)) +} + +// TokenHintGT applies the GT predicate on the "token_hint" field. +func TokenHintGT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldTokenHint, v)) +} + +// TokenHintGTE applies the GTE predicate on the "token_hint" field. +func TokenHintGTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldTokenHint, v)) +} + +// TokenHintLT applies the LT predicate on the "token_hint" field. +func TokenHintLT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldTokenHint, v)) +} + +// TokenHintLTE applies the LTE predicate on the "token_hint" field. +func TokenHintLTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldTokenHint, v)) +} + +// TokenHintContains applies the Contains predicate on the "token_hint" field. +func TokenHintContains(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContains(FieldTokenHint, v)) +} + +// TokenHintHasPrefix applies the HasPrefix predicate on the "token_hint" field. +func TokenHintHasPrefix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasPrefix(FieldTokenHint, v)) +} + +// TokenHintHasSuffix applies the HasSuffix predicate on the "token_hint" field. +func TokenHintHasSuffix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasSuffix(FieldTokenHint, v)) +} + +// TokenHintEqualFold applies the EqualFold predicate on the "token_hint" field. +func TokenHintEqualFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEqualFold(FieldTokenHint, v)) +} + +// TokenHintContainsFold applies the ContainsFold predicate on the "token_hint" field. +func TokenHintContainsFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContainsFold(FieldTokenHint, v)) +} + +// TokenHashEQ applies the EQ predicate on the "token_hash" field. +func TokenHashEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldTokenHash, v)) +} + +// TokenHashNEQ applies the NEQ predicate on the "token_hash" field. +func TokenHashNEQ(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldTokenHash, v)) +} + +// TokenHashIn applies the In predicate on the "token_hash" field. +func TokenHashIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldTokenHash, vs...)) +} + +// TokenHashNotIn applies the NotIn predicate on the "token_hash" field. +func TokenHashNotIn(vs ...string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldTokenHash, vs...)) +} + +// TokenHashGT applies the GT predicate on the "token_hash" field. +func TokenHashGT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldTokenHash, v)) +} + +// TokenHashGTE applies the GTE predicate on the "token_hash" field. +func TokenHashGTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldTokenHash, v)) +} + +// TokenHashLT applies the LT predicate on the "token_hash" field. +func TokenHashLT(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldTokenHash, v)) +} + +// TokenHashLTE applies the LTE predicate on the "token_hash" field. +func TokenHashLTE(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldTokenHash, v)) +} + +// TokenHashContains applies the Contains predicate on the "token_hash" field. +func TokenHashContains(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContains(FieldTokenHash, v)) +} + +// TokenHashHasPrefix applies the HasPrefix predicate on the "token_hash" field. +func TokenHashHasPrefix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasPrefix(FieldTokenHash, v)) +} + +// TokenHashHasSuffix applies the HasSuffix predicate on the "token_hash" field. +func TokenHashHasSuffix(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldHasSuffix(FieldTokenHash, v)) +} + +// TokenHashEqualFold applies the EqualFold predicate on the "token_hash" field. +func TokenHashEqualFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEqualFold(FieldTokenHash, v)) +} + +// TokenHashContainsFold applies the ContainsFold predicate on the "token_hash" field. +func TokenHashContainsFold(v string) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldContainsFold(FieldTokenHash, v)) +} + +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v Status) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v Status) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...Status) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...Status) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldStatus, vs...)) +} + +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldExpiresAt, v)) +} + +// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field. +func ExpiresAtIsNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIsNull(FieldExpiresAt)) +} + +// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field. +func ExpiresAtNotNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotNull(FieldExpiresAt)) +} + +// LastUsedAtEQ applies the EQ predicate on the "last_used_at" field. +func LastUsedAtEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldLastUsedAt, v)) +} + +// LastUsedAtNEQ applies the NEQ predicate on the "last_used_at" field. +func LastUsedAtNEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldLastUsedAt, v)) +} + +// LastUsedAtIn applies the In predicate on the "last_used_at" field. +func LastUsedAtIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldLastUsedAt, vs...)) +} + +// LastUsedAtNotIn applies the NotIn predicate on the "last_used_at" field. +func LastUsedAtNotIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldLastUsedAt, vs...)) +} + +// LastUsedAtGT applies the GT predicate on the "last_used_at" field. +func LastUsedAtGT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldLastUsedAt, v)) +} + +// LastUsedAtGTE applies the GTE predicate on the "last_used_at" field. +func LastUsedAtGTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldLastUsedAt, v)) +} + +// LastUsedAtLT applies the LT predicate on the "last_used_at" field. +func LastUsedAtLT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldLastUsedAt, v)) +} + +// LastUsedAtLTE applies the LTE predicate on the "last_used_at" field. +func LastUsedAtLTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldLastUsedAt, v)) +} + +// LastUsedAtIsNil applies the IsNil predicate on the "last_used_at" field. +func LastUsedAtIsNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIsNull(FieldLastUsedAt)) +} + +// LastUsedAtNotNil applies the NotNil predicate on the "last_used_at" field. +func LastUsedAtNotNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotNull(FieldLastUsedAt)) +} + +// CreatedAtEQ applies the EQ predicate on the "created_at" field. +func CreatedAtEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldCreatedAt, v)) +} + +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. +func CreatedAtNEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldCreatedAt, v)) +} + +// CreatedAtIn applies the In predicate on the "created_at" field. +func CreatedAtIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldCreatedAt, vs...)) +} + +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. +func CreatedAtNotIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldCreatedAt, vs...)) +} + +// CreatedAtGT applies the GT predicate on the "created_at" field. +func CreatedAtGT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldCreatedAt, v)) +} + +// CreatedAtGTE applies the GTE predicate on the "created_at" field. +func CreatedAtGTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldCreatedAt, v)) +} + +// CreatedAtLT applies the LT predicate on the "created_at" field. +func CreatedAtLT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldCreatedAt, v)) +} + +// CreatedAtLTE applies the LTE predicate on the "created_at" field. +func CreatedAtLTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldCreatedAt, v)) +} + +// UpdatedAtEQ applies the EQ predicate on the "updated_at" field. +func UpdatedAtEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field. +func UpdatedAtNEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldUpdatedAt, v)) +} + +// UpdatedAtIn applies the In predicate on the "updated_at" field. +func UpdatedAtIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldUpdatedAt, vs...)) +} + +// UpdatedAtGT applies the GT predicate on the "updated_at" field. +func UpdatedAtGT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldUpdatedAt, v)) +} + +// UpdatedAtGTE applies the GTE predicate on the "updated_at" field. +func UpdatedAtGTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldUpdatedAt, v)) +} + +// UpdatedAtLT applies the LT predicate on the "updated_at" field. +func UpdatedAtLT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldUpdatedAt, v)) +} + +// UpdatedAtLTE applies the LTE predicate on the "updated_at" field. +func UpdatedAtLTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldUpdatedAt, v)) +} + +// DisabledAtEQ applies the EQ predicate on the "disabled_at" field. +func DisabledAtEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldDisabledAt, v)) +} + +// DisabledAtNEQ applies the NEQ predicate on the "disabled_at" field. +func DisabledAtNEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldDisabledAt, v)) +} + +// DisabledAtIn applies the In predicate on the "disabled_at" field. +func DisabledAtIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldDisabledAt, vs...)) +} + +// DisabledAtNotIn applies the NotIn predicate on the "disabled_at" field. +func DisabledAtNotIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldDisabledAt, vs...)) +} + +// DisabledAtGT applies the GT predicate on the "disabled_at" field. +func DisabledAtGT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldDisabledAt, v)) +} + +// DisabledAtGTE applies the GTE predicate on the "disabled_at" field. +func DisabledAtGTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldDisabledAt, v)) +} + +// DisabledAtLT applies the LT predicate on the "disabled_at" field. +func DisabledAtLT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldDisabledAt, v)) +} + +// DisabledAtLTE applies the LTE predicate on the "disabled_at" field. +func DisabledAtLTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldDisabledAt, v)) +} + +// DisabledAtIsNil applies the IsNil predicate on the "disabled_at" field. +func DisabledAtIsNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIsNull(FieldDisabledAt)) +} + +// DisabledAtNotNil applies the NotNil predicate on the "disabled_at" field. +func DisabledAtNotNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotNull(FieldDisabledAt)) +} + +// RevokedAtEQ applies the EQ predicate on the "revoked_at" field. +func RevokedAtEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldEQ(FieldRevokedAt, v)) +} + +// RevokedAtNEQ applies the NEQ predicate on the "revoked_at" field. +func RevokedAtNEQ(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNEQ(FieldRevokedAt, v)) +} + +// RevokedAtIn applies the In predicate on the "revoked_at" field. +func RevokedAtIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIn(FieldRevokedAt, vs...)) +} + +// RevokedAtNotIn applies the NotIn predicate on the "revoked_at" field. +func RevokedAtNotIn(vs ...time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotIn(FieldRevokedAt, vs...)) +} + +// RevokedAtGT applies the GT predicate on the "revoked_at" field. +func RevokedAtGT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGT(FieldRevokedAt, v)) +} + +// RevokedAtGTE applies the GTE predicate on the "revoked_at" field. +func RevokedAtGTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldGTE(FieldRevokedAt, v)) +} + +// RevokedAtLT applies the LT predicate on the "revoked_at" field. +func RevokedAtLT(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLT(FieldRevokedAt, v)) +} + +// RevokedAtLTE applies the LTE predicate on the "revoked_at" field. +func RevokedAtLTE(v time.Time) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldLTE(FieldRevokedAt, v)) +} + +// RevokedAtIsNil applies the IsNil predicate on the "revoked_at" field. +func RevokedAtIsNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldIsNull(FieldRevokedAt)) +} + +// RevokedAtNotNil applies the NotNil predicate on the "revoked_at" field. +func RevokedAtNotNil() predicate.UserAPIKey { + return predicate.UserAPIKey(sql.FieldNotNull(FieldRevokedAt)) +} + +// HasUser applies the HasEdge predicate on the "user" edge. +func HasUser() predicate.UserAPIKey { + return predicate.UserAPIKey(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasUserWith applies the HasEdge predicate on the "user" edge with a given conditions (other predicates). +func HasUserWith(preds ...predicate.User) predicate.UserAPIKey { + return predicate.UserAPIKey(func(s *sql.Selector) { + step := newUserStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + +// HasProject applies the HasEdge predicate on the "project" edge. +func HasProject() predicate.UserAPIKey { + return predicate.UserAPIKey(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, ProjectTable, ProjectColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasProjectWith applies the HasEdge predicate on the "project" edge with a given conditions (other predicates). +func HasProjectWith(preds ...predicate.Project) predicate.UserAPIKey { + return predicate.UserAPIKey(func(s *sql.Selector) { + step := newProjectStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.UserAPIKey) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.AndPredicates(predicates...)) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.UserAPIKey) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.OrPredicates(predicates...)) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.UserAPIKey) predicate.UserAPIKey { + return predicate.UserAPIKey(sql.NotPredicates(p)) +} diff --git a/ent/userapikey_create.go b/ent/userapikey_create.go new file mode 100644 index 000000000..fef0929b0 --- /dev/null +++ b/ent/userapikey_create.go @@ -0,0 +1,512 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/BetterAndBetterII/openase/ent/project" + "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" + "github.com/google/uuid" +) + +// UserAPIKeyCreate is the builder for creating a UserAPIKey entity. +type UserAPIKeyCreate struct { + config + mutation *UserAPIKeyMutation + hooks []Hook +} + +// SetUserID sets the "user_id" field. +func (_c *UserAPIKeyCreate) SetUserID(v uuid.UUID) *UserAPIKeyCreate { + _c.mutation.SetUserID(v) + return _c +} + +// SetProjectID sets the "project_id" field. +func (_c *UserAPIKeyCreate) SetProjectID(v uuid.UUID) *UserAPIKeyCreate { + _c.mutation.SetProjectID(v) + return _c +} + +// SetName sets the "name" field. +func (_c *UserAPIKeyCreate) SetName(v string) *UserAPIKeyCreate { + _c.mutation.SetName(v) + return _c +} + +// SetTokenPrefix sets the "token_prefix" field. +func (_c *UserAPIKeyCreate) SetTokenPrefix(v string) *UserAPIKeyCreate { + _c.mutation.SetTokenPrefix(v) + return _c +} + +// SetTokenHint sets the "token_hint" field. +func (_c *UserAPIKeyCreate) SetTokenHint(v string) *UserAPIKeyCreate { + _c.mutation.SetTokenHint(v) + return _c +} + +// SetTokenHash sets the "token_hash" field. +func (_c *UserAPIKeyCreate) SetTokenHash(v string) *UserAPIKeyCreate { + _c.mutation.SetTokenHash(v) + return _c +} + +// SetScopes sets the "scopes" field. +func (_c *UserAPIKeyCreate) SetScopes(v []string) *UserAPIKeyCreate { + _c.mutation.SetScopes(v) + return _c +} + +// SetStatus sets the "status" field. +func (_c *UserAPIKeyCreate) SetStatus(v userapikey.Status) *UserAPIKeyCreate { + _c.mutation.SetStatus(v) + return _c +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableStatus(v *userapikey.Status) *UserAPIKeyCreate { + if v != nil { + _c.SetStatus(*v) + } + return _c +} + +// SetExpiresAt sets the "expires_at" field. +func (_c *UserAPIKeyCreate) SetExpiresAt(v time.Time) *UserAPIKeyCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableExpiresAt(v *time.Time) *UserAPIKeyCreate { + if v != nil { + _c.SetExpiresAt(*v) + } + return _c +} + +// SetLastUsedAt sets the "last_used_at" field. +func (_c *UserAPIKeyCreate) SetLastUsedAt(v time.Time) *UserAPIKeyCreate { + _c.mutation.SetLastUsedAt(v) + return _c +} + +// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableLastUsedAt(v *time.Time) *UserAPIKeyCreate { + if v != nil { + _c.SetLastUsedAt(*v) + } + return _c +} + +// SetCreatedAt sets the "created_at" field. +func (_c *UserAPIKeyCreate) SetCreatedAt(v time.Time) *UserAPIKeyCreate { + _c.mutation.SetCreatedAt(v) + return _c +} + +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableCreatedAt(v *time.Time) *UserAPIKeyCreate { + if v != nil { + _c.SetCreatedAt(*v) + } + return _c +} + +// SetUpdatedAt sets the "updated_at" field. +func (_c *UserAPIKeyCreate) SetUpdatedAt(v time.Time) *UserAPIKeyCreate { + _c.mutation.SetUpdatedAt(v) + return _c +} + +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableUpdatedAt(v *time.Time) *UserAPIKeyCreate { + if v != nil { + _c.SetUpdatedAt(*v) + } + return _c +} + +// SetDisabledAt sets the "disabled_at" field. +func (_c *UserAPIKeyCreate) SetDisabledAt(v time.Time) *UserAPIKeyCreate { + _c.mutation.SetDisabledAt(v) + return _c +} + +// SetNillableDisabledAt sets the "disabled_at" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableDisabledAt(v *time.Time) *UserAPIKeyCreate { + if v != nil { + _c.SetDisabledAt(*v) + } + return _c +} + +// SetRevokedAt sets the "revoked_at" field. +func (_c *UserAPIKeyCreate) SetRevokedAt(v time.Time) *UserAPIKeyCreate { + _c.mutation.SetRevokedAt(v) + return _c +} + +// SetNillableRevokedAt sets the "revoked_at" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableRevokedAt(v *time.Time) *UserAPIKeyCreate { + if v != nil { + _c.SetRevokedAt(*v) + } + return _c +} + +// SetID sets the "id" field. +func (_c *UserAPIKeyCreate) SetID(v uuid.UUID) *UserAPIKeyCreate { + _c.mutation.SetID(v) + return _c +} + +// SetNillableID sets the "id" field if the given value is not nil. +func (_c *UserAPIKeyCreate) SetNillableID(v *uuid.UUID) *UserAPIKeyCreate { + if v != nil { + _c.SetID(*v) + } + return _c +} + +// SetUser sets the "user" edge to the User entity. +func (_c *UserAPIKeyCreate) SetUser(v *User) *UserAPIKeyCreate { + return _c.SetUserID(v.ID) +} + +// SetProject sets the "project" edge to the Project entity. +func (_c *UserAPIKeyCreate) SetProject(v *Project) *UserAPIKeyCreate { + return _c.SetProjectID(v.ID) +} + +// Mutation returns the UserAPIKeyMutation object of the builder. +func (_c *UserAPIKeyCreate) Mutation() *UserAPIKeyMutation { + return _c.mutation +} + +// Save creates the UserAPIKey in the database. +func (_c *UserAPIKeyCreate) Save(ctx context.Context) (*UserAPIKey, error) { + _c.defaults() + return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks) +} + +// SaveX calls Save and panics if Save returns an error. +func (_c *UserAPIKeyCreate) SaveX(ctx context.Context) *UserAPIKey { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *UserAPIKeyCreate) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *UserAPIKeyCreate) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_c *UserAPIKeyCreate) defaults() { + if _, ok := _c.mutation.Scopes(); !ok { + v := userapikey.DefaultScopes + _c.mutation.SetScopes(v) + } + if _, ok := _c.mutation.Status(); !ok { + v := userapikey.DefaultStatus + _c.mutation.SetStatus(v) + } + if _, ok := _c.mutation.CreatedAt(); !ok { + v := userapikey.DefaultCreatedAt() + _c.mutation.SetCreatedAt(v) + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + v := userapikey.DefaultUpdatedAt() + _c.mutation.SetUpdatedAt(v) + } + if _, ok := _c.mutation.ID(); !ok { + v := userapikey.DefaultID() + _c.mutation.SetID(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_c *UserAPIKeyCreate) check() error { + if _, ok := _c.mutation.UserID(); !ok { + return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "UserAPIKey.user_id"`)} + } + if _, ok := _c.mutation.ProjectID(); !ok { + return &ValidationError{Name: "project_id", err: errors.New(`ent: missing required field "UserAPIKey.project_id"`)} + } + if _, ok := _c.mutation.Name(); !ok { + return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "UserAPIKey.name"`)} + } + if v, ok := _c.mutation.Name(); ok { + if err := userapikey.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.name": %w`, err)} + } + } + if _, ok := _c.mutation.TokenPrefix(); !ok { + return &ValidationError{Name: "token_prefix", err: errors.New(`ent: missing required field "UserAPIKey.token_prefix"`)} + } + if v, ok := _c.mutation.TokenPrefix(); ok { + if err := userapikey.TokenPrefixValidator(v); err != nil { + return &ValidationError{Name: "token_prefix", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_prefix": %w`, err)} + } + } + if _, ok := _c.mutation.TokenHint(); !ok { + return &ValidationError{Name: "token_hint", err: errors.New(`ent: missing required field "UserAPIKey.token_hint"`)} + } + if v, ok := _c.mutation.TokenHint(); ok { + if err := userapikey.TokenHintValidator(v); err != nil { + return &ValidationError{Name: "token_hint", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_hint": %w`, err)} + } + } + if _, ok := _c.mutation.TokenHash(); !ok { + return &ValidationError{Name: "token_hash", err: errors.New(`ent: missing required field "UserAPIKey.token_hash"`)} + } + if v, ok := _c.mutation.TokenHash(); ok { + if err := userapikey.TokenHashValidator(v); err != nil { + return &ValidationError{Name: "token_hash", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_hash": %w`, err)} + } + } + if _, ok := _c.mutation.Scopes(); !ok { + return &ValidationError{Name: "scopes", err: errors.New(`ent: missing required field "UserAPIKey.scopes"`)} + } + if _, ok := _c.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "UserAPIKey.status"`)} + } + if v, ok := _c.mutation.Status(); ok { + if err := userapikey.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.status": %w`, err)} + } + } + if _, ok := _c.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "UserAPIKey.created_at"`)} + } + if _, ok := _c.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "UserAPIKey.updated_at"`)} + } + if len(_c.mutation.UserIDs()) == 0 { + return &ValidationError{Name: "user", err: errors.New(`ent: missing required edge "UserAPIKey.user"`)} + } + if len(_c.mutation.ProjectIDs()) == 0 { + return &ValidationError{Name: "project", err: errors.New(`ent: missing required edge "UserAPIKey.project"`)} + } + return nil +} + +func (_c *UserAPIKeyCreate) sqlSave(ctx context.Context) (*UserAPIKey, error) { + if err := _c.check(); err != nil { + return nil, err + } + _node, _spec := _c.createSpec() + if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + if _spec.ID.Value != nil { + if id, ok := _spec.ID.Value.(*uuid.UUID); ok { + _node.ID = *id + } else if err := _node.ID.Scan(_spec.ID.Value); err != nil { + return nil, err + } + } + _c.mutation.id = &_node.ID + _c.mutation.done = true + return _node, nil +} + +func (_c *UserAPIKeyCreate) createSpec() (*UserAPIKey, *sqlgraph.CreateSpec) { + var ( + _node = &UserAPIKey{config: _c.config} + _spec = sqlgraph.NewCreateSpec(userapikey.Table, sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID)) + ) + if id, ok := _c.mutation.ID(); ok { + _node.ID = id + _spec.ID.Value = &id + } + if value, ok := _c.mutation.Name(); ok { + _spec.SetField(userapikey.FieldName, field.TypeString, value) + _node.Name = value + } + if value, ok := _c.mutation.TokenPrefix(); ok { + _spec.SetField(userapikey.FieldTokenPrefix, field.TypeString, value) + _node.TokenPrefix = value + } + if value, ok := _c.mutation.TokenHint(); ok { + _spec.SetField(userapikey.FieldTokenHint, field.TypeString, value) + _node.TokenHint = value + } + if value, ok := _c.mutation.TokenHash(); ok { + _spec.SetField(userapikey.FieldTokenHash, field.TypeString, value) + _node.TokenHash = value + } + if value, ok := _c.mutation.Scopes(); ok { + _spec.SetField(userapikey.FieldScopes, field.TypeJSON, value) + _node.Scopes = value + } + if value, ok := _c.mutation.Status(); ok { + _spec.SetField(userapikey.FieldStatus, field.TypeEnum, value) + _node.Status = value + } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(userapikey.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = &value + } + if value, ok := _c.mutation.LastUsedAt(); ok { + _spec.SetField(userapikey.FieldLastUsedAt, field.TypeTime, value) + _node.LastUsedAt = &value + } + if value, ok := _c.mutation.CreatedAt(); ok { + _spec.SetField(userapikey.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := _c.mutation.UpdatedAt(); ok { + _spec.SetField(userapikey.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + if value, ok := _c.mutation.DisabledAt(); ok { + _spec.SetField(userapikey.FieldDisabledAt, field.TypeTime, value) + _node.DisabledAt = &value + } + if value, ok := _c.mutation.RevokedAt(); ok { + _spec.SetField(userapikey.FieldRevokedAt, field.TypeTime, value) + _node.RevokedAt = &value + } + if nodes := _c.mutation.UserIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.UserTable, + Columns: []string{userapikey.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.UserID = nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } + if nodes := _c.mutation.ProjectIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.ProjectTable, + Columns: []string{userapikey.ProjectColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.ProjectID = nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } + return _node, _spec +} + +// UserAPIKeyCreateBulk is the builder for creating many UserAPIKey entities in bulk. +type UserAPIKeyCreateBulk struct { + config + err error + builders []*UserAPIKeyCreate +} + +// Save creates the UserAPIKey entities in the database. +func (_c *UserAPIKeyCreateBulk) Save(ctx context.Context) ([]*UserAPIKey, error) { + if _c.err != nil { + return nil, _c.err + } + specs := make([]*sqlgraph.CreateSpec, len(_c.builders)) + nodes := make([]*UserAPIKey, len(_c.builders)) + mutators := make([]Mutator, len(_c.builders)) + for i := range _c.builders { + func(i int, root context.Context) { + builder := _c.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*UserAPIKeyMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + var err error + nodes[i], specs[i] = builder.createSpec() + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (_c *UserAPIKeyCreateBulk) SaveX(ctx context.Context) []*UserAPIKey { + v, err := _c.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (_c *UserAPIKeyCreateBulk) Exec(ctx context.Context) error { + _, err := _c.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_c *UserAPIKeyCreateBulk) ExecX(ctx context.Context) { + if err := _c.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/userapikey_delete.go b/ent/userapikey_delete.go new file mode 100644 index 000000000..b2bd8922b --- /dev/null +++ b/ent/userapikey_delete.go @@ -0,0 +1,88 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/BetterAndBetterII/openase/ent/predicate" + "github.com/BetterAndBetterII/openase/ent/userapikey" +) + +// UserAPIKeyDelete is the builder for deleting a UserAPIKey entity. +type UserAPIKeyDelete struct { + config + hooks []Hook + mutation *UserAPIKeyMutation +} + +// Where appends a list predicates to the UserAPIKeyDelete builder. +func (_d *UserAPIKeyDelete) Where(ps ...predicate.UserAPIKey) *UserAPIKeyDelete { + _d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (_d *UserAPIKeyDelete) Exec(ctx context.Context) (int, error) { + return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks) +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *UserAPIKeyDelete) ExecX(ctx context.Context) int { + n, err := _d.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (_d *UserAPIKeyDelete) sqlExec(ctx context.Context) (int, error) { + _spec := sqlgraph.NewDeleteSpec(userapikey.Table, sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID)) + if ps := _d.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + _d.mutation.done = true + return affected, err +} + +// UserAPIKeyDeleteOne is the builder for deleting a single UserAPIKey entity. +type UserAPIKeyDeleteOne struct { + _d *UserAPIKeyDelete +} + +// Where appends a list predicates to the UserAPIKeyDelete builder. +func (_d *UserAPIKeyDeleteOne) Where(ps ...predicate.UserAPIKey) *UserAPIKeyDeleteOne { + _d._d.mutation.Where(ps...) + return _d +} + +// Exec executes the deletion query. +func (_d *UserAPIKeyDeleteOne) Exec(ctx context.Context) error { + n, err := _d._d.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{userapikey.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (_d *UserAPIKeyDeleteOne) ExecX(ctx context.Context) { + if err := _d.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/ent/userapikey_query.go b/ent/userapikey_query.go new file mode 100644 index 000000000..8822b8312 --- /dev/null +++ b/ent/userapikey_query.go @@ -0,0 +1,682 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/BetterAndBetterII/openase/ent/predicate" + "github.com/BetterAndBetterII/openase/ent/project" + "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" + "github.com/google/uuid" +) + +// UserAPIKeyQuery is the builder for querying UserAPIKey entities. +type UserAPIKeyQuery struct { + config + ctx *QueryContext + order []userapikey.OrderOption + inters []Interceptor + predicates []predicate.UserAPIKey + withUser *UserQuery + withProject *ProjectQuery + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the UserAPIKeyQuery builder. +func (_q *UserAPIKeyQuery) Where(ps ...predicate.UserAPIKey) *UserAPIKeyQuery { + _q.predicates = append(_q.predicates, ps...) + return _q +} + +// Limit the number of records to be returned by this query. +func (_q *UserAPIKeyQuery) Limit(limit int) *UserAPIKeyQuery { + _q.ctx.Limit = &limit + return _q +} + +// Offset to start from. +func (_q *UserAPIKeyQuery) Offset(offset int) *UserAPIKeyQuery { + _q.ctx.Offset = &offset + return _q +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (_q *UserAPIKeyQuery) Unique(unique bool) *UserAPIKeyQuery { + _q.ctx.Unique = &unique + return _q +} + +// Order specifies how the records should be ordered. +func (_q *UserAPIKeyQuery) Order(o ...userapikey.OrderOption) *UserAPIKeyQuery { + _q.order = append(_q.order, o...) + return _q +} + +// QueryUser chains the current query on the "user" edge. +func (_q *UserAPIKeyQuery) QueryUser() *UserQuery { + query := (&UserClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(userapikey.Table, userapikey.FieldID, selector), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, userapikey.UserTable, userapikey.UserColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + +// QueryProject chains the current query on the "project" edge. +func (_q *UserAPIKeyQuery) QueryProject() *ProjectQuery { + query := (&ProjectClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(userapikey.Table, userapikey.FieldID, selector), + sqlgraph.To(project.Table, project.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, userapikey.ProjectTable, userapikey.ProjectColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + +// First returns the first UserAPIKey entity from the query. +// Returns a *NotFoundError when no UserAPIKey was found. +func (_q *UserAPIKeyQuery) First(ctx context.Context) (*UserAPIKey, error) { + nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst)) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{userapikey.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (_q *UserAPIKeyQuery) FirstX(ctx context.Context) *UserAPIKey { + node, err := _q.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first UserAPIKey ID from the query. +// Returns a *NotFoundError when no UserAPIKey ID was found. +func (_q *UserAPIKeyQuery) FirstID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{userapikey.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (_q *UserAPIKeyQuery) FirstIDX(ctx context.Context) uuid.UUID { + id, err := _q.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single UserAPIKey entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one UserAPIKey entity is found. +// Returns a *NotFoundError when no UserAPIKey entities are found. +func (_q *UserAPIKeyQuery) Only(ctx context.Context) (*UserAPIKey, error) { + nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly)) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{userapikey.Label} + default: + return nil, &NotSingularError{userapikey.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (_q *UserAPIKeyQuery) OnlyX(ctx context.Context) *UserAPIKey { + node, err := _q.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only UserAPIKey ID in the query. +// Returns a *NotSingularError when more than one UserAPIKey ID is found. +// Returns a *NotFoundError when no entities are found. +func (_q *UserAPIKeyQuery) OnlyID(ctx context.Context) (id uuid.UUID, err error) { + var ids []uuid.UUID + if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{userapikey.Label} + default: + err = &NotSingularError{userapikey.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (_q *UserAPIKeyQuery) OnlyIDX(ctx context.Context) uuid.UUID { + id, err := _q.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of UserAPIKeys. +func (_q *UserAPIKeyQuery) All(ctx context.Context) ([]*UserAPIKey, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll) + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + qr := querierAll[[]*UserAPIKey, *UserAPIKeyQuery]() + return withInterceptors[[]*UserAPIKey](ctx, _q, qr, _q.inters) +} + +// AllX is like All, but panics if an error occurs. +func (_q *UserAPIKeyQuery) AllX(ctx context.Context) []*UserAPIKey { + nodes, err := _q.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of UserAPIKey IDs. +func (_q *UserAPIKeyQuery) IDs(ctx context.Context) (ids []uuid.UUID, err error) { + if _q.ctx.Unique == nil && _q.path != nil { + _q.Unique(true) + } + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs) + if err = _q.Select(userapikey.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (_q *UserAPIKeyQuery) IDsX(ctx context.Context) []uuid.UUID { + ids, err := _q.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (_q *UserAPIKeyQuery) Count(ctx context.Context) (int, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount) + if err := _q.prepareQuery(ctx); err != nil { + return 0, err + } + return withInterceptors[int](ctx, _q, querierCount[*UserAPIKeyQuery](), _q.inters) +} + +// CountX is like Count, but panics if an error occurs. +func (_q *UserAPIKeyQuery) CountX(ctx context.Context) int { + count, err := _q.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (_q *UserAPIKeyQuery) Exist(ctx context.Context) (bool, error) { + ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist) + switch _, err := _q.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +// ExistX is like Exist, but panics if an error occurs. +func (_q *UserAPIKeyQuery) ExistX(ctx context.Context) bool { + exist, err := _q.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the UserAPIKeyQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (_q *UserAPIKeyQuery) Clone() *UserAPIKeyQuery { + if _q == nil { + return nil + } + return &UserAPIKeyQuery{ + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]userapikey.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.UserAPIKey{}, _q.predicates...), + withUser: _q.withUser.Clone(), + withProject: _q.withProject.Clone(), + // clone intermediate query. + sql: _q.sql.Clone(), + path: _q.path, + } +} + +// WithUser tells the query-builder to eager-load the nodes that are connected to +// the "user" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *UserAPIKeyQuery) WithUser(opts ...func(*UserQuery)) *UserAPIKeyQuery { + query := (&UserClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withUser = query + return _q +} + +// WithProject tells the query-builder to eager-load the nodes that are connected to +// the "project" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *UserAPIKeyQuery) WithProject(opts ...func(*ProjectQuery)) *UserAPIKeyQuery { + query := (&ProjectClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withProject = query + return _q +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// UserID uuid.UUID `json:"user_id,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.UserAPIKey.Query(). +// GroupBy(userapikey.FieldUserID). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (_q *UserAPIKeyQuery) GroupBy(field string, fields ...string) *UserAPIKeyGroupBy { + _q.ctx.Fields = append([]string{field}, fields...) + grbuild := &UserAPIKeyGroupBy{build: _q} + grbuild.flds = &_q.ctx.Fields + grbuild.label = userapikey.Label + grbuild.scan = grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// UserID uuid.UUID `json:"user_id,omitempty"` +// } +// +// client.UserAPIKey.Query(). +// Select(userapikey.FieldUserID). +// Scan(ctx, &v) +func (_q *UserAPIKeyQuery) Select(fields ...string) *UserAPIKeySelect { + _q.ctx.Fields = append(_q.ctx.Fields, fields...) + sbuild := &UserAPIKeySelect{UserAPIKeyQuery: _q} + sbuild.label = userapikey.Label + sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan + return sbuild +} + +// Aggregate returns a UserAPIKeySelect configured with the given aggregations. +func (_q *UserAPIKeyQuery) Aggregate(fns ...AggregateFunc) *UserAPIKeySelect { + return _q.Select().Aggregate(fns...) +} + +func (_q *UserAPIKeyQuery) prepareQuery(ctx context.Context) error { + for _, inter := range _q.inters { + if inter == nil { + return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)") + } + if trv, ok := inter.(Traverser); ok { + if err := trv.Traverse(ctx, _q); err != nil { + return err + } + } + } + for _, f := range _q.ctx.Fields { + if !userapikey.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if _q.path != nil { + prev, err := _q.path(ctx) + if err != nil { + return err + } + _q.sql = prev + } + return nil +} + +func (_q *UserAPIKeyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*UserAPIKey, error) { + var ( + nodes = []*UserAPIKey{} + _spec = _q.querySpec() + loadedTypes = [2]bool{ + _q.withUser != nil, + _q.withProject != nil, + } + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*UserAPIKey).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &UserAPIKey{config: _q.config} + nodes = append(nodes, node) + node.Edges.loadedTypes = loadedTypes + return node.assignValues(columns, values) + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + if query := _q.withUser; query != nil { + if err := _q.loadUser(ctx, query, nodes, nil, + func(n *UserAPIKey, e *User) { n.Edges.User = e }); err != nil { + return nil, err + } + } + if query := _q.withProject; query != nil { + if err := _q.loadProject(ctx, query, nodes, nil, + func(n *UserAPIKey, e *Project) { n.Edges.Project = e }); err != nil { + return nil, err + } + } + return nodes, nil +} + +func (_q *UserAPIKeyQuery) loadUser(ctx context.Context, query *UserQuery, nodes []*UserAPIKey, init func(*UserAPIKey), assign func(*UserAPIKey, *User)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*UserAPIKey) + for i := range nodes { + fk := nodes[i].UserID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(user.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "user_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} +func (_q *UserAPIKeyQuery) loadProject(ctx context.Context, query *ProjectQuery, nodes []*UserAPIKey, init func(*UserAPIKey), assign func(*UserAPIKey, *Project)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*UserAPIKey) + for i := range nodes { + fk := nodes[i].ProjectID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(project.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "project_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} + +func (_q *UserAPIKeyQuery) sqlCount(ctx context.Context) (int, error) { + _spec := _q.querySpec() + _spec.Node.Columns = _q.ctx.Fields + if len(_q.ctx.Fields) > 0 { + _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique + } + return sqlgraph.CountNodes(ctx, _q.driver, _spec) +} + +func (_q *UserAPIKeyQuery) querySpec() *sqlgraph.QuerySpec { + _spec := sqlgraph.NewQuerySpec(userapikey.Table, userapikey.Columns, sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID)) + _spec.From = _q.sql + if unique := _q.ctx.Unique; unique != nil { + _spec.Unique = *unique + } else if _q.path != nil { + _spec.Unique = true + } + if fields := _q.ctx.Fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, userapikey.FieldID) + for i := range fields { + if fields[i] != userapikey.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + if _q.withUser != nil { + _spec.Node.AddColumnOnce(userapikey.FieldUserID) + } + if _q.withProject != nil { + _spec.Node.AddColumnOnce(userapikey.FieldProjectID) + } + } + if ps := _q.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := _q.ctx.Limit; limit != nil { + _spec.Limit = *limit + } + if offset := _q.ctx.Offset; offset != nil { + _spec.Offset = *offset + } + if ps := _q.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (_q *UserAPIKeyQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(_q.driver.Dialect()) + t1 := builder.Table(userapikey.Table) + columns := _q.ctx.Fields + if len(columns) == 0 { + columns = userapikey.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if _q.sql != nil { + selector = _q.sql + selector.Select(selector.Columns(columns...)...) + } + if _q.ctx.Unique != nil && *_q.ctx.Unique { + selector.Distinct() + } + for _, p := range _q.predicates { + p(selector) + } + for _, p := range _q.order { + p(selector) + } + if offset := _q.ctx.Offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := _q.ctx.Limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// UserAPIKeyGroupBy is the group-by builder for UserAPIKey entities. +type UserAPIKeyGroupBy struct { + selector + build *UserAPIKeyQuery +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (_g *UserAPIKeyGroupBy) Aggregate(fns ...AggregateFunc) *UserAPIKeyGroupBy { + _g.fns = append(_g.fns, fns...) + return _g +} + +// Scan applies the selector query and scans the result into the given value. +func (_g *UserAPIKeyGroupBy) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy) + if err := _g.build.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*UserAPIKeyQuery, *UserAPIKeyGroupBy](ctx, _g.build, _g, _g.build.inters, v) +} + +func (_g *UserAPIKeyGroupBy) sqlScan(ctx context.Context, root *UserAPIKeyQuery, v any) error { + selector := root.sqlQuery(ctx).Select() + aggregation := make([]string, 0, len(_g.fns)) + for _, fn := range _g.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(*_g.flds)+len(_g.fns)) + for _, f := range *_g.flds { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + selector.GroupBy(selector.Columns(*_g.flds...)...) + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _g.build.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +// UserAPIKeySelect is the builder for selecting fields of UserAPIKey entities. +type UserAPIKeySelect struct { + *UserAPIKeyQuery + selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (_s *UserAPIKeySelect) Aggregate(fns ...AggregateFunc) *UserAPIKeySelect { + _s.fns = append(_s.fns, fns...) + return _s +} + +// Scan applies the selector query and scans the result into the given value. +func (_s *UserAPIKeySelect) Scan(ctx context.Context, v any) error { + ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect) + if err := _s.prepareQuery(ctx); err != nil { + return err + } + return scanWithInterceptors[*UserAPIKeyQuery, *UserAPIKeySelect](ctx, _s.UserAPIKeyQuery, _s, _s.inters, v) +} + +func (_s *UserAPIKeySelect) sqlScan(ctx context.Context, root *UserAPIKeyQuery, v any) error { + selector := root.sqlQuery(ctx) + aggregation := make([]string, 0, len(_s.fns)) + for _, fn := range _s.fns { + aggregation = append(aggregation, fn(selector)) + } + switch n := len(*_s.selector.flds); { + case n == 0 && len(aggregation) > 0: + selector.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + selector.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := _s.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/ent/userapikey_update.go b/ent/userapikey_update.go new file mode 100644 index 000000000..60ffc1d72 --- /dev/null +++ b/ent/userapikey_update.go @@ -0,0 +1,928 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/dialect/sql/sqljson" + "entgo.io/ent/schema/field" + "github.com/BetterAndBetterII/openase/ent/predicate" + "github.com/BetterAndBetterII/openase/ent/project" + "github.com/BetterAndBetterII/openase/ent/user" + "github.com/BetterAndBetterII/openase/ent/userapikey" + "github.com/google/uuid" +) + +// UserAPIKeyUpdate is the builder for updating UserAPIKey entities. +type UserAPIKeyUpdate struct { + config + hooks []Hook + mutation *UserAPIKeyMutation +} + +// Where appends a list predicates to the UserAPIKeyUpdate builder. +func (_u *UserAPIKeyUpdate) Where(ps ...predicate.UserAPIKey) *UserAPIKeyUpdate { + _u.mutation.Where(ps...) + return _u +} + +// SetUserID sets the "user_id" field. +func (_u *UserAPIKeyUpdate) SetUserID(v uuid.UUID) *UserAPIKeyUpdate { + _u.mutation.SetUserID(v) + return _u +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableUserID(v *uuid.UUID) *UserAPIKeyUpdate { + if v != nil { + _u.SetUserID(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *UserAPIKeyUpdate) SetProjectID(v uuid.UUID) *UserAPIKeyUpdate { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableProjectID(v *uuid.UUID) *UserAPIKeyUpdate { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *UserAPIKeyUpdate) SetName(v string) *UserAPIKeyUpdate { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableName(v *string) *UserAPIKeyUpdate { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetTokenPrefix sets the "token_prefix" field. +func (_u *UserAPIKeyUpdate) SetTokenPrefix(v string) *UserAPIKeyUpdate { + _u.mutation.SetTokenPrefix(v) + return _u +} + +// SetNillableTokenPrefix sets the "token_prefix" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableTokenPrefix(v *string) *UserAPIKeyUpdate { + if v != nil { + _u.SetTokenPrefix(*v) + } + return _u +} + +// SetTokenHint sets the "token_hint" field. +func (_u *UserAPIKeyUpdate) SetTokenHint(v string) *UserAPIKeyUpdate { + _u.mutation.SetTokenHint(v) + return _u +} + +// SetNillableTokenHint sets the "token_hint" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableTokenHint(v *string) *UserAPIKeyUpdate { + if v != nil { + _u.SetTokenHint(*v) + } + return _u +} + +// SetTokenHash sets the "token_hash" field. +func (_u *UserAPIKeyUpdate) SetTokenHash(v string) *UserAPIKeyUpdate { + _u.mutation.SetTokenHash(v) + return _u +} + +// SetNillableTokenHash sets the "token_hash" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableTokenHash(v *string) *UserAPIKeyUpdate { + if v != nil { + _u.SetTokenHash(*v) + } + return _u +} + +// SetScopes sets the "scopes" field. +func (_u *UserAPIKeyUpdate) SetScopes(v []string) *UserAPIKeyUpdate { + _u.mutation.SetScopes(v) + return _u +} + +// AppendScopes appends value to the "scopes" field. +func (_u *UserAPIKeyUpdate) AppendScopes(v []string) *UserAPIKeyUpdate { + _u.mutation.AppendScopes(v) + return _u +} + +// SetStatus sets the "status" field. +func (_u *UserAPIKeyUpdate) SetStatus(v userapikey.Status) *UserAPIKeyUpdate { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableStatus(v *userapikey.Status) *UserAPIKeyUpdate { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *UserAPIKeyUpdate) SetExpiresAt(v time.Time) *UserAPIKeyUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableExpiresAt(v *time.Time) *UserAPIKeyUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *UserAPIKeyUpdate) ClearExpiresAt() *UserAPIKeyUpdate { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetLastUsedAt sets the "last_used_at" field. +func (_u *UserAPIKeyUpdate) SetLastUsedAt(v time.Time) *UserAPIKeyUpdate { + _u.mutation.SetLastUsedAt(v) + return _u +} + +// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableLastUsedAt(v *time.Time) *UserAPIKeyUpdate { + if v != nil { + _u.SetLastUsedAt(*v) + } + return _u +} + +// ClearLastUsedAt clears the value of the "last_used_at" field. +func (_u *UserAPIKeyUpdate) ClearLastUsedAt() *UserAPIKeyUpdate { + _u.mutation.ClearLastUsedAt() + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *UserAPIKeyUpdate) SetUpdatedAt(v time.Time) *UserAPIKeyUpdate { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetDisabledAt sets the "disabled_at" field. +func (_u *UserAPIKeyUpdate) SetDisabledAt(v time.Time) *UserAPIKeyUpdate { + _u.mutation.SetDisabledAt(v) + return _u +} + +// SetNillableDisabledAt sets the "disabled_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableDisabledAt(v *time.Time) *UserAPIKeyUpdate { + if v != nil { + _u.SetDisabledAt(*v) + } + return _u +} + +// ClearDisabledAt clears the value of the "disabled_at" field. +func (_u *UserAPIKeyUpdate) ClearDisabledAt() *UserAPIKeyUpdate { + _u.mutation.ClearDisabledAt() + return _u +} + +// SetRevokedAt sets the "revoked_at" field. +func (_u *UserAPIKeyUpdate) SetRevokedAt(v time.Time) *UserAPIKeyUpdate { + _u.mutation.SetRevokedAt(v) + return _u +} + +// SetNillableRevokedAt sets the "revoked_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdate) SetNillableRevokedAt(v *time.Time) *UserAPIKeyUpdate { + if v != nil { + _u.SetRevokedAt(*v) + } + return _u +} + +// ClearRevokedAt clears the value of the "revoked_at" field. +func (_u *UserAPIKeyUpdate) ClearRevokedAt() *UserAPIKeyUpdate { + _u.mutation.ClearRevokedAt() + return _u +} + +// SetUser sets the "user" edge to the User entity. +func (_u *UserAPIKeyUpdate) SetUser(v *User) *UserAPIKeyUpdate { + return _u.SetUserID(v.ID) +} + +// SetProject sets the "project" edge to the Project entity. +func (_u *UserAPIKeyUpdate) SetProject(v *Project) *UserAPIKeyUpdate { + return _u.SetProjectID(v.ID) +} + +// Mutation returns the UserAPIKeyMutation object of the builder. +func (_u *UserAPIKeyUpdate) Mutation() *UserAPIKeyMutation { + return _u.mutation +} + +// ClearUser clears the "user" edge to the User entity. +func (_u *UserAPIKeyUpdate) ClearUser() *UserAPIKeyUpdate { + _u.mutation.ClearUser() + return _u +} + +// ClearProject clears the "project" edge to the Project entity. +func (_u *UserAPIKeyUpdate) ClearProject() *UserAPIKeyUpdate { + _u.mutation.ClearProject() + return _u +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (_u *UserAPIKeyUpdate) Save(ctx context.Context) (int, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *UserAPIKeyUpdate) SaveX(ctx context.Context) int { + affected, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (_u *UserAPIKeyUpdate) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *UserAPIKeyUpdate) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *UserAPIKeyUpdate) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := userapikey.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *UserAPIKeyUpdate) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := userapikey.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.name": %w`, err)} + } + } + if v, ok := _u.mutation.TokenPrefix(); ok { + if err := userapikey.TokenPrefixValidator(v); err != nil { + return &ValidationError{Name: "token_prefix", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_prefix": %w`, err)} + } + } + if v, ok := _u.mutation.TokenHint(); ok { + if err := userapikey.TokenHintValidator(v); err != nil { + return &ValidationError{Name: "token_hint", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_hint": %w`, err)} + } + } + if v, ok := _u.mutation.TokenHash(); ok { + if err := userapikey.TokenHashValidator(v); err != nil { + return &ValidationError{Name: "token_hash", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_hash": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := userapikey.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.status": %w`, err)} + } + } + if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "UserAPIKey.user"`) + } + if _u.mutation.ProjectCleared() && len(_u.mutation.ProjectIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "UserAPIKey.project"`) + } + return nil +} + +func (_u *UserAPIKeyUpdate) sqlSave(ctx context.Context) (_node int, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(userapikey.Table, userapikey.Columns, sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID)) + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(userapikey.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.TokenPrefix(); ok { + _spec.SetField(userapikey.FieldTokenPrefix, field.TypeString, value) + } + if value, ok := _u.mutation.TokenHint(); ok { + _spec.SetField(userapikey.FieldTokenHint, field.TypeString, value) + } + if value, ok := _u.mutation.TokenHash(); ok { + _spec.SetField(userapikey.FieldTokenHash, field.TypeString, value) + } + if value, ok := _u.mutation.Scopes(); ok { + _spec.SetField(userapikey.FieldScopes, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedScopes(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, userapikey.FieldScopes, value) + }) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(userapikey.FieldStatus, field.TypeEnum, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(userapikey.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(userapikey.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.LastUsedAt(); ok { + _spec.SetField(userapikey.FieldLastUsedAt, field.TypeTime, value) + } + if _u.mutation.LastUsedAtCleared() { + _spec.ClearField(userapikey.FieldLastUsedAt, field.TypeTime) + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(userapikey.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.DisabledAt(); ok { + _spec.SetField(userapikey.FieldDisabledAt, field.TypeTime, value) + } + if _u.mutation.DisabledAtCleared() { + _spec.ClearField(userapikey.FieldDisabledAt, field.TypeTime) + } + if value, ok := _u.mutation.RevokedAt(); ok { + _spec.SetField(userapikey.FieldRevokedAt, field.TypeTime, value) + } + if _u.mutation.RevokedAtCleared() { + _spec.ClearField(userapikey.FieldRevokedAt, field.TypeTime) + } + if _u.mutation.UserCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.UserTable, + Columns: []string{userapikey.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.UserIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.UserTable, + Columns: []string{userapikey.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + if _u.mutation.ProjectCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.ProjectTable, + Columns: []string{userapikey.ProjectColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.ProjectIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.ProjectTable, + Columns: []string{userapikey.ProjectColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{userapikey.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + _u.mutation.done = true + return _node, nil +} + +// UserAPIKeyUpdateOne is the builder for updating a single UserAPIKey entity. +type UserAPIKeyUpdateOne struct { + config + fields []string + hooks []Hook + mutation *UserAPIKeyMutation +} + +// SetUserID sets the "user_id" field. +func (_u *UserAPIKeyUpdateOne) SetUserID(v uuid.UUID) *UserAPIKeyUpdateOne { + _u.mutation.SetUserID(v) + return _u +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableUserID(v *uuid.UUID) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetUserID(*v) + } + return _u +} + +// SetProjectID sets the "project_id" field. +func (_u *UserAPIKeyUpdateOne) SetProjectID(v uuid.UUID) *UserAPIKeyUpdateOne { + _u.mutation.SetProjectID(v) + return _u +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableProjectID(v *uuid.UUID) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetProjectID(*v) + } + return _u +} + +// SetName sets the "name" field. +func (_u *UserAPIKeyUpdateOne) SetName(v string) *UserAPIKeyUpdateOne { + _u.mutation.SetName(v) + return _u +} + +// SetNillableName sets the "name" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableName(v *string) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetName(*v) + } + return _u +} + +// SetTokenPrefix sets the "token_prefix" field. +func (_u *UserAPIKeyUpdateOne) SetTokenPrefix(v string) *UserAPIKeyUpdateOne { + _u.mutation.SetTokenPrefix(v) + return _u +} + +// SetNillableTokenPrefix sets the "token_prefix" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableTokenPrefix(v *string) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetTokenPrefix(*v) + } + return _u +} + +// SetTokenHint sets the "token_hint" field. +func (_u *UserAPIKeyUpdateOne) SetTokenHint(v string) *UserAPIKeyUpdateOne { + _u.mutation.SetTokenHint(v) + return _u +} + +// SetNillableTokenHint sets the "token_hint" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableTokenHint(v *string) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetTokenHint(*v) + } + return _u +} + +// SetTokenHash sets the "token_hash" field. +func (_u *UserAPIKeyUpdateOne) SetTokenHash(v string) *UserAPIKeyUpdateOne { + _u.mutation.SetTokenHash(v) + return _u +} + +// SetNillableTokenHash sets the "token_hash" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableTokenHash(v *string) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetTokenHash(*v) + } + return _u +} + +// SetScopes sets the "scopes" field. +func (_u *UserAPIKeyUpdateOne) SetScopes(v []string) *UserAPIKeyUpdateOne { + _u.mutation.SetScopes(v) + return _u +} + +// AppendScopes appends value to the "scopes" field. +func (_u *UserAPIKeyUpdateOne) AppendScopes(v []string) *UserAPIKeyUpdateOne { + _u.mutation.AppendScopes(v) + return _u +} + +// SetStatus sets the "status" field. +func (_u *UserAPIKeyUpdateOne) SetStatus(v userapikey.Status) *UserAPIKeyUpdateOne { + _u.mutation.SetStatus(v) + return _u +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableStatus(v *userapikey.Status) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetStatus(*v) + } + return _u +} + +// SetExpiresAt sets the "expires_at" field. +func (_u *UserAPIKeyUpdateOne) SetExpiresAt(v time.Time) *UserAPIKeyUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableExpiresAt(v *time.Time) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *UserAPIKeyUpdateOne) ClearExpiresAt() *UserAPIKeyUpdateOne { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetLastUsedAt sets the "last_used_at" field. +func (_u *UserAPIKeyUpdateOne) SetLastUsedAt(v time.Time) *UserAPIKeyUpdateOne { + _u.mutation.SetLastUsedAt(v) + return _u +} + +// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableLastUsedAt(v *time.Time) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetLastUsedAt(*v) + } + return _u +} + +// ClearLastUsedAt clears the value of the "last_used_at" field. +func (_u *UserAPIKeyUpdateOne) ClearLastUsedAt() *UserAPIKeyUpdateOne { + _u.mutation.ClearLastUsedAt() + return _u +} + +// SetUpdatedAt sets the "updated_at" field. +func (_u *UserAPIKeyUpdateOne) SetUpdatedAt(v time.Time) *UserAPIKeyUpdateOne { + _u.mutation.SetUpdatedAt(v) + return _u +} + +// SetDisabledAt sets the "disabled_at" field. +func (_u *UserAPIKeyUpdateOne) SetDisabledAt(v time.Time) *UserAPIKeyUpdateOne { + _u.mutation.SetDisabledAt(v) + return _u +} + +// SetNillableDisabledAt sets the "disabled_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableDisabledAt(v *time.Time) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetDisabledAt(*v) + } + return _u +} + +// ClearDisabledAt clears the value of the "disabled_at" field. +func (_u *UserAPIKeyUpdateOne) ClearDisabledAt() *UserAPIKeyUpdateOne { + _u.mutation.ClearDisabledAt() + return _u +} + +// SetRevokedAt sets the "revoked_at" field. +func (_u *UserAPIKeyUpdateOne) SetRevokedAt(v time.Time) *UserAPIKeyUpdateOne { + _u.mutation.SetRevokedAt(v) + return _u +} + +// SetNillableRevokedAt sets the "revoked_at" field if the given value is not nil. +func (_u *UserAPIKeyUpdateOne) SetNillableRevokedAt(v *time.Time) *UserAPIKeyUpdateOne { + if v != nil { + _u.SetRevokedAt(*v) + } + return _u +} + +// ClearRevokedAt clears the value of the "revoked_at" field. +func (_u *UserAPIKeyUpdateOne) ClearRevokedAt() *UserAPIKeyUpdateOne { + _u.mutation.ClearRevokedAt() + return _u +} + +// SetUser sets the "user" edge to the User entity. +func (_u *UserAPIKeyUpdateOne) SetUser(v *User) *UserAPIKeyUpdateOne { + return _u.SetUserID(v.ID) +} + +// SetProject sets the "project" edge to the Project entity. +func (_u *UserAPIKeyUpdateOne) SetProject(v *Project) *UserAPIKeyUpdateOne { + return _u.SetProjectID(v.ID) +} + +// Mutation returns the UserAPIKeyMutation object of the builder. +func (_u *UserAPIKeyUpdateOne) Mutation() *UserAPIKeyMutation { + return _u.mutation +} + +// ClearUser clears the "user" edge to the User entity. +func (_u *UserAPIKeyUpdateOne) ClearUser() *UserAPIKeyUpdateOne { + _u.mutation.ClearUser() + return _u +} + +// ClearProject clears the "project" edge to the Project entity. +func (_u *UserAPIKeyUpdateOne) ClearProject() *UserAPIKeyUpdateOne { + _u.mutation.ClearProject() + return _u +} + +// Where appends a list predicates to the UserAPIKeyUpdate builder. +func (_u *UserAPIKeyUpdateOne) Where(ps ...predicate.UserAPIKey) *UserAPIKeyUpdateOne { + _u.mutation.Where(ps...) + return _u +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (_u *UserAPIKeyUpdateOne) Select(field string, fields ...string) *UserAPIKeyUpdateOne { + _u.fields = append([]string{field}, fields...) + return _u +} + +// Save executes the query and returns the updated UserAPIKey entity. +func (_u *UserAPIKeyUpdateOne) Save(ctx context.Context) (*UserAPIKey, error) { + _u.defaults() + return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks) +} + +// SaveX is like Save, but panics if an error occurs. +func (_u *UserAPIKeyUpdateOne) SaveX(ctx context.Context) *UserAPIKey { + node, err := _u.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (_u *UserAPIKeyUpdateOne) Exec(ctx context.Context) error { + _, err := _u.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (_u *UserAPIKeyUpdateOne) ExecX(ctx context.Context) { + if err := _u.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (_u *UserAPIKeyUpdateOne) defaults() { + if _, ok := _u.mutation.UpdatedAt(); !ok { + v := userapikey.UpdateDefaultUpdatedAt() + _u.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (_u *UserAPIKeyUpdateOne) check() error { + if v, ok := _u.mutation.Name(); ok { + if err := userapikey.NameValidator(v); err != nil { + return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.name": %w`, err)} + } + } + if v, ok := _u.mutation.TokenPrefix(); ok { + if err := userapikey.TokenPrefixValidator(v); err != nil { + return &ValidationError{Name: "token_prefix", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_prefix": %w`, err)} + } + } + if v, ok := _u.mutation.TokenHint(); ok { + if err := userapikey.TokenHintValidator(v); err != nil { + return &ValidationError{Name: "token_hint", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_hint": %w`, err)} + } + } + if v, ok := _u.mutation.TokenHash(); ok { + if err := userapikey.TokenHashValidator(v); err != nil { + return &ValidationError{Name: "token_hash", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.token_hash": %w`, err)} + } + } + if v, ok := _u.mutation.Status(); ok { + if err := userapikey.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "UserAPIKey.status": %w`, err)} + } + } + if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "UserAPIKey.user"`) + } + if _u.mutation.ProjectCleared() && len(_u.mutation.ProjectIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "UserAPIKey.project"`) + } + return nil +} + +func (_u *UserAPIKeyUpdateOne) sqlSave(ctx context.Context) (_node *UserAPIKey, err error) { + if err := _u.check(); err != nil { + return _node, err + } + _spec := sqlgraph.NewUpdateSpec(userapikey.Table, userapikey.Columns, sqlgraph.NewFieldSpec(userapikey.FieldID, field.TypeUUID)) + id, ok := _u.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "UserAPIKey.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := _u.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, userapikey.FieldID) + for _, f := range fields { + if !userapikey.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != userapikey.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := _u.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := _u.mutation.Name(); ok { + _spec.SetField(userapikey.FieldName, field.TypeString, value) + } + if value, ok := _u.mutation.TokenPrefix(); ok { + _spec.SetField(userapikey.FieldTokenPrefix, field.TypeString, value) + } + if value, ok := _u.mutation.TokenHint(); ok { + _spec.SetField(userapikey.FieldTokenHint, field.TypeString, value) + } + if value, ok := _u.mutation.TokenHash(); ok { + _spec.SetField(userapikey.FieldTokenHash, field.TypeString, value) + } + if value, ok := _u.mutation.Scopes(); ok { + _spec.SetField(userapikey.FieldScopes, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedScopes(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, userapikey.FieldScopes, value) + }) + } + if value, ok := _u.mutation.Status(); ok { + _spec.SetField(userapikey.FieldStatus, field.TypeEnum, value) + } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(userapikey.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(userapikey.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.LastUsedAt(); ok { + _spec.SetField(userapikey.FieldLastUsedAt, field.TypeTime, value) + } + if _u.mutation.LastUsedAtCleared() { + _spec.ClearField(userapikey.FieldLastUsedAt, field.TypeTime) + } + if value, ok := _u.mutation.UpdatedAt(); ok { + _spec.SetField(userapikey.FieldUpdatedAt, field.TypeTime, value) + } + if value, ok := _u.mutation.DisabledAt(); ok { + _spec.SetField(userapikey.FieldDisabledAt, field.TypeTime, value) + } + if _u.mutation.DisabledAtCleared() { + _spec.ClearField(userapikey.FieldDisabledAt, field.TypeTime) + } + if value, ok := _u.mutation.RevokedAt(); ok { + _spec.SetField(userapikey.FieldRevokedAt, field.TypeTime, value) + } + if _u.mutation.RevokedAtCleared() { + _spec.ClearField(userapikey.FieldRevokedAt, field.TypeTime) + } + if _u.mutation.UserCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.UserTable, + Columns: []string{userapikey.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.UserIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.UserTable, + Columns: []string{userapikey.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + if _u.mutation.ProjectCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.ProjectTable, + Columns: []string{userapikey.ProjectColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.ProjectIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: userapikey.ProjectTable, + Columns: []string{userapikey.ProjectColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } + _node = &UserAPIKey{config: _u.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{userapikey.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + _u.mutation.done = true + return _node, nil +} diff --git a/internal/agentplatform/ports.go b/internal/agentplatform/ports.go index 8ec965245..755790274 100644 --- a/internal/agentplatform/ports.go +++ b/internal/agentplatform/ports.go @@ -14,5 +14,7 @@ type Repository interface { CreateToken(ctx context.Context, record domain.CreateTokenRecord) error TokenByHash(ctx context.Context, tokenHash string) (domain.StoredTokenRecord, error) TouchTokenLastUsed(ctx context.Context, tokenID uuid.UUID, usedAt time.Time) error + UserAPIKeyByHash(ctx context.Context, tokenHash string) (domain.StoredUserAPIKeyRecord, error) + TouchUserAPIKeyLastUsed(ctx context.Context, keyID uuid.UUID, usedAt time.Time) error ProjectTokenInventory(ctx context.Context, projectID uuid.UUID, now time.Time) (domain.ProjectTokenInventory, error) } diff --git a/internal/agentplatform/service.go b/internal/agentplatform/service.go index bf6cc3998..b8c0786e5 100644 --- a/internal/agentplatform/service.go +++ b/internal/agentplatform/service.go @@ -84,12 +84,14 @@ const ( const ( PrincipalKindTicketAgent = domain.PrincipalKindTicketAgent PrincipalKindProjectConversation = domain.PrincipalKindProjectConversation + PrincipalKindUserAPIKey = domain.PrincipalKindUserAPIKey ) var ( ErrUnavailable = errors.New("agent platform service unavailable") ErrTokenNotFound = errors.New("agent token not found") ErrTokenExpired = errors.New("agent token expired") + ErrTokenDisabled = errors.New("agent token is disabled") ErrInvalidToken = errors.New("agent token is invalid") ErrInvalidScope = errors.New("agent token scope is invalid") ErrInvalidPrincipal = errors.New("agent token principal is invalid") @@ -261,6 +263,10 @@ func (s *Service) Authenticate(ctx context.Context, rawToken string) (Claims, er return Claims{}, err } + if strings.HasPrefix(tokenText, domain.UserAPIKeyTokenPrefix) { + return s.authenticateUserAPIKey(ctx, tokenText) + } + record, err := s.repo.TokenByHash(ctx, hashToken(tokenText)) if err != nil { if errors.Is(err, domain.ErrNotFound) { @@ -346,7 +352,10 @@ func (s *Service) ProjectTokenInventory(ctx context.Context, projectID uuid.UUID func ParseToken(raw string) (string, error) { trimmed := strings.TrimSpace(raw) - if trimmed == "" || !strings.HasPrefix(trimmed, TokenPrefix) { + if trimmed == "" { + return "", ErrInvalidToken + } + if !strings.HasPrefix(trimmed, TokenPrefix) && !strings.HasPrefix(trimmed, domain.UserAPIKeyTokenPrefix) { return "", ErrInvalidToken } return trimmed, nil @@ -404,6 +413,18 @@ func SupportedScopeGroupsForPrincipalKind(kind PrincipalKind) []ScopeGroup { return scopeGroupsFor(supportedScopesForPrincipalKind(kind)) } +func ParseExplicitScopesForPrincipalKind(kind PrincipalKind, raw []string) ([]string, error) { + parsed, err := parseExplicitScopesForPrincipalKind(kind, raw) + if err != nil { + return nil, err + } + return scopeStrings(parsed), nil +} + +func UserAPIKeyTokenPrefix() string { + return domain.UserAPIKeyTokenPrefix +} + func parseScopes(raw []string) (ScopeSet, error) { return parseScopesForPrincipalKind(PrincipalKindTicketAgent, raw) } @@ -469,15 +490,31 @@ func resolveTTL(raw time.Duration) time.Duration { } func generateToken(rng io.Reader) (string, string, error) { + return generateTokenWithPrefix(rng, TokenPrefix) +} + +func GenerateOpaqueToken(prefix string, rng io.Reader) (string, string, error) { + return generateTokenWithPrefix(rng, prefix) +} + +func generateTokenWithPrefix(rng io.Reader, prefix string) (string, string, error) { bytes := make([]byte, 24) if _, err := io.ReadFull(rng, bytes); err != nil { return "", "", fmt.Errorf("generate agent token bytes: %w", err) } - token := TokenPrefix + base64.RawURLEncoding.EncodeToString(bytes) + token := prefix + base64.RawURLEncoding.EncodeToString(bytes) return token, hashToken(token), nil } +func TokenPreview(token string) string { + trimmed := strings.TrimSpace(token) + if len(trimmed) <= 18 { + return trimmed + } + return trimmed[:12] + "..." + trimmed[len(trimmed)-4:] +} + func hashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) @@ -618,6 +655,45 @@ func uuidPointer(value uuid.UUID) *uuid.UUID { return &value } +func (s *Service) authenticateUserAPIKey(ctx context.Context, rawToken string) (Claims, error) { + record, err := s.repo.UserAPIKeyByHash(ctx, hashToken(rawToken)) + if err != nil { + if errors.Is(err, domain.ErrNotFound) { + return Claims{}, ErrTokenNotFound + } + return Claims{}, fmt.Errorf("load user api key: %w", err) + } + if record.Status != "active" { + return Claims{}, ErrTokenDisabled + } + if record.ExpiresAt != nil && record.ExpiresAt.Before(s.now().UTC()) { + return Claims{}, ErrTokenExpired + } + scopes, err := parseExplicitScopesForPrincipalKind(PrincipalKindUserAPIKey, record.Scopes) + if err != nil { + return Claims{}, err + } + if err := s.repo.TouchUserAPIKeyLastUsed(ctx, record.ID, s.now().UTC()); err != nil { + return Claims{}, fmt.Errorf("touch user api key last_used_at: %w", err) + } + return Claims{ + TokenID: record.ID, + PrincipalKind: PrincipalKindUserAPIKey, + PrincipalID: record.UserID, + PrincipalName: record.Name, + ProjectID: record.ProjectID, + Scopes: scopeStrings(scopes), + ExpiresAt: timePointerValue(record.ExpiresAt), + }, nil +} + +func timePointerValue(value *time.Time) time.Time { + if value == nil { + return time.Time{} + } + return value.UTC() +} + func uuidPointerValue(value *uuid.UUID) uuid.UUID { if value == nil { return uuid.Nil diff --git a/internal/app/app.go b/internal/app/app.go index 762f7a9ba..bcbe84b5c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -43,6 +43,7 @@ import ( secretsrepo "github.com/BetterAndBetterII/openase/internal/repo/secrets" ticketrepo "github.com/BetterAndBetterII/openase/internal/repo/ticket" ticketstatusrepo "github.com/BetterAndBetterII/openase/internal/repo/ticketstatus" + userapikeyrepo "github.com/BetterAndBetterII/openase/internal/repo/userapikey" workflowrepo "github.com/BetterAndBetterII/openase/internal/repo/workflow" "github.com/BetterAndBetterII/openase/internal/runtime/database" runtimeobservability "github.com/BetterAndBetterII/openase/internal/runtime/observability" @@ -54,6 +55,7 @@ import ( githubreposervice "github.com/BetterAndBetterII/openase/internal/service/githubrepo" humanauthservice "github.com/BetterAndBetterII/openase/internal/service/humanauth" secretsservice "github.com/BetterAndBetterII/openase/internal/service/secrets" + userapikeyservice "github.com/BetterAndBetterII/openase/internal/service/userapikey" ticketservice "github.com/BetterAndBetterII/openase/internal/ticket" "github.com/BetterAndBetterII/openase/internal/ticketstatus" workflowservice "github.com/BetterAndBetterII/openase/internal/workflow" @@ -315,12 +317,14 @@ func (a *App) RunServe(ctx context.Context) error { humanAuthSvc = humanauthservice.NewService(humanAuthRepo, http.DefaultClient, instanceAuthSvc) humanAuthorizer = humanauthservice.NewAuthorizer(humanAuthRepo) } + userAPIKeySvc := userapikeyservice.NewService(userapikeyrepo.NewEntRepository(client), humanAuthorizer) machineChannelSvc := machinechannelservice.NewService(machinechannelrepo.NewEntRepository(client)) machineSessions := machinechannelservice.NewSessionRegistry(machinechannelservice.DefaultHeartbeatTimeout) serverOpts := []httpapi.ServerOption{ httpapi.WithGitHubAuthService(githubAuthSvc), httpapi.WithGitHubRepoService(githubRepoSvc), httpapi.WithSecretService(secretSvc), + httpapi.WithUserAPIKeyService(userAPIKeySvc), httpapi.WithInstanceAuthService(instanceAuthSvc), httpapi.WithRuntimeConfigFile(a.config.Metadata.ConfigFile), httpapi.WithHomeDir(homeDir), diff --git a/internal/domain/agentplatform/contracts.go b/internal/domain/agentplatform/contracts.go index cdc4da8bc..6a12b8ac8 100644 --- a/internal/domain/agentplatform/contracts.go +++ b/internal/domain/agentplatform/contracts.go @@ -10,7 +10,10 @@ import ( "github.com/google/uuid" ) -const TokenPrefix = "ase_agent_" +const ( + TokenPrefix = "ase_agent_" + UserAPIKeyTokenPrefix = "ase_pat_" +) const ( EnvAPIURL = "OPENASE_API_URL" @@ -98,6 +101,7 @@ type PrincipalKind string const ( PrincipalKindTicketAgent PrincipalKind = "ticket_agent" PrincipalKindProjectConversation PrincipalKind = "project_conversation" + PrincipalKindUserAPIKey PrincipalKind = "user_api_key" ) type ScopeSet []Scope @@ -120,6 +124,8 @@ func DefaultScopesForPrincipalKind(kind PrincipalKind) []string { switch kind { case PrincipalKindProjectConversation: return SupportedScopesForPrincipalKind(kind) + case PrincipalKindUserAPIKey: + return nil default: return DefaultAgentScopes() } @@ -201,6 +207,18 @@ func SupportedScopesForPrincipalKind(kind PrincipalKind) []string { scopes = append(scopes, scope) } return scopes + case PrincipalKindUserAPIKey: + return []string{ + string(ScopeActivityRead), + string(ScopeProjectUpdatesRead), + string(ScopeProjectUpdatesWrite), + string(ScopeReposRead), + string(ScopeStatusesList), + string(ScopeTicketsCreate), + string(ScopeTicketsList), + string(ScopeTicketsUpdate), + string(ScopeWorkflowsRead), + } default: return SupportedAgentScopes() } @@ -303,6 +321,17 @@ type StoredTokenRecord struct { LastUsedAt *time.Time } +type StoredUserAPIKeyRecord struct { + ID uuid.UUID + UserID uuid.UUID + ProjectID uuid.UUID + Name string + Scopes []string + Status string + ExpiresAt *time.Time + LastUsedAt *time.Time +} + func (c Claims) HasScope(scope Scope) bool { for _, item := range c.Scopes { if item == string(scope) { @@ -319,6 +348,8 @@ func (c Claims) CreatedBy() string { return c.PrincipalName } return "project-conversation:" + c.PrincipalName + case PrincipalKindUserAPIKey: + return "user:" + c.PrincipalID.String() + " via user-api-key:" + strings.TrimSpace(c.PrincipalName) default: return "agent:" + c.PrincipalName } diff --git a/internal/domain/userapikey/contracts.go b/internal/domain/userapikey/contracts.go new file mode 100644 index 000000000..ad3a3e9ff --- /dev/null +++ b/internal/domain/userapikey/contracts.go @@ -0,0 +1,149 @@ +package userapikey + +import ( + "fmt" + "strings" + "time" + + agentplatformdomain "github.com/BetterAndBetterII/openase/internal/domain/agentplatform" + "github.com/google/uuid" +) + +type Status string + +const ( + StatusActive Status = "active" + StatusDisabled Status = "disabled" + StatusRevoked Status = "revoked" +) + +type APIKey struct { + ID uuid.UUID + UserID uuid.UUID + ProjectID uuid.UUID + Name string + TokenPrefix string + TokenHint string + Scopes []string + Status Status + ExpiresAt *time.Time + LastUsedAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time + DisabledAt *time.Time + RevokedAt *time.Time +} + +type CreateInput struct { + ProjectID uuid.UUID + UserID uuid.UUID + Name string + Scopes []string + ExpiresAt *time.Time +} + +type CreateResult struct { + APIKey APIKey + PlainTextToken string +} + +type ParseCreateInput struct { + ProjectID string + UserID uuid.UUID + Name string + Scopes []string + ExpiresAt *string +} + +func ParseCreate(raw ParseCreateInput) (CreateInput, error) { + projectID, err := uuid.Parse(strings.TrimSpace(raw.ProjectID)) + if err != nil { + return CreateInput{}, fmt.Errorf("project_id must be a valid UUID") + } + if raw.UserID == uuid.Nil { + return CreateInput{}, fmt.Errorf("user_id must be a valid UUID") + } + name := strings.TrimSpace(raw.Name) + if name == "" { + return CreateInput{}, fmt.Errorf("name must not be empty") + } + requestedScopes := make([]string, 0, len(raw.Scopes)) + for _, item := range raw.Scopes { + scope := strings.TrimSpace(item) + if scope == "" { + return CreateInput{}, fmt.Errorf("scopes must not contain empty values") + } + if !contains(requestedScopes, scope) { + requestedScopes = append(requestedScopes, scope) + } + } + if len(requestedScopes) == 0 { + return CreateInput{}, fmt.Errorf("at least one scope must be selected") + } + + var expiresAt *time.Time + if raw.ExpiresAt != nil && strings.TrimSpace(*raw.ExpiresAt) != "" { + parsed, err := time.Parse(time.RFC3339, strings.TrimSpace(*raw.ExpiresAt)) + if err != nil { + return CreateInput{}, fmt.Errorf("expires_at must be RFC3339") + } + utc := parsed.UTC() + expiresAt = &utc + } + + return CreateInput{ + ProjectID: projectID, + UserID: raw.UserID, + Name: name, + Scopes: requestedScopes, + ExpiresAt: expiresAt, + }, nil +} + +func SupportedScopeGroups(scopes []string) []agentplatformdomain.ScopeGroup { + if len(scopes) == 0 { + return nil + } + groups := make(map[string][]string) + for _, scope := range scopes { + category, _, found := strings.Cut(scope, ".") + if !found { + category = scope + } + groups[category] = append(groups[category], scope) + } + categories := make([]string, 0, len(groups)) + for category := range groups { + categories = append(categories, category) + } + // small local sort avoids a new utility dependency here. + for i := 0; i < len(categories); i++ { + for j := i + 1; j < len(categories); j++ { + if categories[j] < categories[i] { + categories[i], categories[j] = categories[j], categories[i] + } + } + } + result := make([]agentplatformdomain.ScopeGroup, 0, len(categories)) + for _, category := range categories { + items := append([]string(nil), groups[category]...) + for i := 0; i < len(items); i++ { + for j := i + 1; j < len(items); j++ { + if items[j] < items[i] { + items[i], items[j] = items[j], items[i] + } + } + } + result = append(result, agentplatformdomain.ScopeGroup{Category: category, Scopes: items}) + } + return result +} + +func contains(items []string, candidate string) bool { + for _, item := range items { + if item == candidate { + return true + } + } + return false +} diff --git a/internal/httpapi/agent_platform_api.go b/internal/httpapi/agent_platform_api.go index 90430ab7e..22c571d1e 100644 --- a/internal/httpapi/agent_platform_api.go +++ b/internal/httpapi/agent_platform_api.go @@ -1092,6 +1092,8 @@ func writeAgentPlatformError(c echo.Context, err error) error { return writeAPIError(c, http.StatusUnauthorized, "AGENT_TOKEN_NOT_FOUND", err.Error()) case errors.Is(err, agentplatform.ErrTokenExpired): return writeAPIError(c, http.StatusUnauthorized, "AGENT_TOKEN_EXPIRED", err.Error()) + case errors.Is(err, agentplatform.ErrTokenDisabled): + return writeAPIError(c, http.StatusUnauthorized, "AGENT_TOKEN_DISABLED", err.Error()) case errors.Is(err, agentplatform.ErrInvalidScope): return writeAPIError(c, http.StatusForbidden, "AGENT_SCOPE_INVALID", err.Error()) case errors.Is(err, agentplatform.ErrInvalidPrincipal): diff --git a/internal/httpapi/agent_platform_api_test.go b/internal/httpapi/agent_platform_api_test.go index a335b60c7..103392f1d 100644 --- a/internal/httpapi/agent_platform_api_test.go +++ b/internal/httpapi/agent_platform_api_test.go @@ -22,6 +22,7 @@ import ( entagentprovider "github.com/BetterAndBetterII/openase/ent/agentprovider" entagentrun "github.com/BetterAndBetterII/openase/ent/agentrun" entticketrepoworkspace "github.com/BetterAndBetterII/openase/ent/ticketrepoworkspace" + entuserapikey "github.com/BetterAndBetterII/openase/ent/userapikey" activitysvc "github.com/BetterAndBetterII/openase/internal/activity" "github.com/BetterAndBetterII/openase/internal/agentplatform" "github.com/BetterAndBetterII/openase/internal/config" @@ -343,6 +344,137 @@ func TestAgentPlatformTicketRoutesRespectScopesAndBoundaries(t *testing.T) { } } +func TestAgentPlatformAcceptsProjectScopedUserAPIKeys(t *testing.T) { + client := openTestEntClient(t) + ctx := context.Background() + projectID, agentID, currentTicketID, _ := seedAgentPlatformHTTPFixture(ctx, t, client) + platformService := agentplatform.NewService(agentplatformrepo.NewEntRepository(client)) + server := NewServer( + config.ServerConfig{Port: 40023}, + config.GitHubConfig{}, + slog.New(slog.NewTextHandler(io.Discard, nil)), + eventinfra.NewChannelBus(), + newTicketService(client), + newTicketStatusService(client), + platformService, + catalogservice.New(catalogrepo.NewEntRepository(client), executable.NewPathResolver(), nil), + nil, + ) + + userID := uuid.New() + if _, err := client.User.Create().SetID(userID).SetPrimaryEmail("user@example.com").SetDisplayName("User").Save(ctx); err != nil { + t.Fatalf("create user: %v", err) + } + token, tokenHash, err := agentplatform.GenerateOpaqueToken(agentplatform.UserAPIKeyTokenPrefix(), strings.NewReader(strings.Repeat("a", 48))) + if err != nil { + t.Fatalf("generate user api key token: %v", err) + } + if _, err := client.UserAPIKey.Create(). + SetUserID(userID). + SetProjectID(projectID). + SetName("Buildkite"). + SetTokenPrefix(agentplatform.UserAPIKeyTokenPrefix()). + SetTokenHint(agentplatform.TokenPreview(token)). + SetTokenHash(tokenHash). + SetScopes([]string{string(agentplatform.ScopeTicketsList)}). + SetStatus(entuserapikey.StatusActive). + Save(ctx); err != nil { + t.Fatalf("create user api key: %v", err) + } + + listResp := struct { + Tickets []ticketResponse `json:"tickets"` + }{} + executeJSONWithHeaders( + t, + server, + http.MethodGet, + fmt.Sprintf("/api/v1/platform/projects/%s/tickets", projectID), + nil, + map[string]string{echo.HeaderAuthorization: "Bearer " + token}, + http.StatusOK, + &listResp, + ) + if len(listResp.Tickets) == 0 { + t.Fatalf("expected project tickets for user api key, got %+v", listResp.Tickets) + } + + scopeDenied := performJSONRequestWithHeaders( + t, + server, + http.MethodPatch, + fmt.Sprintf("/api/v1/platform/tickets/%s", currentTicketID), + `{"description":"blocked"}`, + map[string]string{ + echo.HeaderAuthorization: "Bearer " + token, + echo.HeaderContentType: echo.MIMEApplicationJSON, + }, + ) + if scopeDenied.Code != http.StatusForbidden || !strings.Contains(scopeDenied.Body.String(), "AGENT_SCOPE_FORBIDDEN") { + t.Fatalf("expected scope denied for user api key update, got %d: %s", scopeDenied.Code, scopeDenied.Body.String()) + } + + otherProjectID := uuid.New() + currentProject, err := client.Project.Get(ctx, projectID) + if err != nil { + t.Fatalf("load current project: %v", err) + } + if _, err := client.Project.Create().SetID(otherProjectID).SetOrganizationID(currentProject.OrganizationID).SetName("Other").SetSlug("other").Save(ctx); err != nil { + t.Fatalf("create other project: %v", err) + } + projectDenied := performJSONRequestWithHeaders( + t, + server, + http.MethodGet, + fmt.Sprintf("/api/v1/platform/projects/%s/tickets", otherProjectID), + "", + map[string]string{echo.HeaderAuthorization: "Bearer " + token}, + ) + if projectDenied.Code != http.StatusForbidden || !strings.Contains(projectDenied.Body.String(), "AGENT_PROJECT_FORBIDDEN") { + t.Fatalf("expected project denied for user api key, got %d: %s", projectDenied.Code, projectDenied.Body.String()) + } + + if _, err := client.UserAPIKey.Update(). + Where(entuserapikey.UserIDEQ(userID)). + SetStatus(entuserapikey.StatusDisabled). + Save(ctx); err != nil { + t.Fatalf("disable user api key: %v", err) + } + disabledRec := performJSONRequestWithHeaders( + t, + server, + http.MethodGet, + fmt.Sprintf("/api/v1/platform/projects/%s/tickets", projectID), + "", + map[string]string{echo.HeaderAuthorization: "Bearer " + token}, + ) + if disabledRec.Code != http.StatusUnauthorized || !strings.Contains(disabledRec.Body.String(), "AGENT_TOKEN_DISABLED") { + t.Fatalf("expected disabled user api key rejection, got %d: %s", disabledRec.Code, disabledRec.Body.String()) + } + + expiredAt := time.Now().UTC().Add(-time.Minute) + if _, err := client.UserAPIKey.Update(). + Where(entuserapikey.UserIDEQ(userID)). + SetStatus(entuserapikey.StatusActive). + SetNillableExpiresAt(&expiredAt). + Save(ctx); err != nil { + t.Fatalf("expire user api key: %v", err) + } + expiredRec := performJSONRequestWithHeaders( + t, + server, + http.MethodGet, + fmt.Sprintf("/api/v1/platform/projects/%s/tickets", projectID), + "", + map[string]string{echo.HeaderAuthorization: "Bearer " + token}, + ) + if expiredRec.Code != http.StatusUnauthorized || !strings.Contains(expiredRec.Body.String(), "AGENT_TOKEN_EXPIRED") { + t.Fatalf("expected expired user api key rejection, got %d: %s", expiredRec.Code, expiredRec.Body.String()) + } + + _ = agentID +} + func TestAgentPlatformTicketDependencyRoutesRespectScopes(t *testing.T) { client := openTestEntClient(t) ctx := context.Background() diff --git a/internal/httpapi/authorization.go b/internal/httpapi/authorization.go index 9b9a35030..5ee9bc7ef 100644 --- a/internal/httpapi/authorization.go +++ b/internal/httpapi/authorization.go @@ -241,7 +241,7 @@ func humanRouteAuthorizationRuleFor(path string, method string) (humanRouteAutho permission: channelPermissionForPath(path, method), checkRequired: true, }, true - case "/api/v1/projects/:projectId", "/api/v1/projects/:projectId/activity", "/api/v1/projects/:projectId/events/stream", "/api/v1/projects/:projectId/updates", "/api/v1/projects/:projectId/updates/:threadId", "/api/v1/projects/:projectId/updates/:threadId/revisions", "/api/v1/projects/:projectId/updates/:threadId/comments", "/api/v1/projects/:projectId/updates/:threadId/comments/:commentId", "/api/v1/projects/:projectId/updates/:threadId/comments/:commentId/revisions", "/api/v1/projects/:projectId/notification-rules", "/api/v1/projects/:projectId/scheduled-jobs", "/api/v1/projects/:projectId/skills", "/api/v1/projects/:projectId/skills/import", "/api/v1/projects/:projectId/skills/refresh", "/api/v1/projects/:projectId/workflows", "/api/v1/projects/:projectId/statuses", "/api/v1/projects/:projectId/statuses/reset", "/api/v1/projects/:projectId/tickets", "/api/v1/projects/:projectId/tickets/archived", "/api/v1/projects/:projectId/tickets/:ticketId/detail", "/api/v1/projects/:projectId/tickets/:ticketId/repo-scopes", "/api/v1/projects/:projectId/tickets/:ticketId/runs", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId/raw-events", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId/activities", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId/transcript", "/api/v1/projects/:projectId/repos", "/api/v1/projects/:projectId/token-usage", "/api/v1/projects/:projectId/github/namespaces", "/api/v1/projects/:projectId/github/repos", "/api/v1/projects/:projectId/agents", "/api/v1/projects/:projectId/agent-runs", "/api/v1/projects/:projectId/agents/:agentId/output", "/api/v1/projects/:projectId/agents/:agentId/output/stream", "/api/v1/projects/:projectId/agents/:agentId/steps", "/api/v1/projects/:projectId/agents/:agentId/steps/stream", "/api/v1/projects/:projectId/security-settings", "/api/v1/projects/:projectId/security-settings/secrets", "/api/v1/projects/:projectId/security-settings/secrets/resolve-for-runtime", "/api/v1/projects/:projectId/security-settings/secret-bindings", "/api/v1/projects/:projectId/security-settings/github-outbound-credential", "/api/v1/projects/:projectId/security-settings/github-outbound-credential/import-gh-cli", "/api/v1/projects/:projectId/security-settings/github-outbound-credential/retest", "/api/v1/projects/:projectId/hr-advisor", "/api/v1/projects/:projectId/hr-advisor/activate", "/api/v1/projects/:projectId/role-bindings", "/api/v1/projects/:projectId/role-bindings/:bindingId", "/api/v1/chat/projects/:projectId/conversations/stream": + case "/api/v1/projects/:projectId", "/api/v1/projects/:projectId/activity", "/api/v1/projects/:projectId/events/stream", "/api/v1/projects/:projectId/updates", "/api/v1/projects/:projectId/updates/:threadId", "/api/v1/projects/:projectId/updates/:threadId/revisions", "/api/v1/projects/:projectId/updates/:threadId/comments", "/api/v1/projects/:projectId/updates/:threadId/comments/:commentId", "/api/v1/projects/:projectId/updates/:threadId/comments/:commentId/revisions", "/api/v1/projects/:projectId/notification-rules", "/api/v1/projects/:projectId/scheduled-jobs", "/api/v1/projects/:projectId/skills", "/api/v1/projects/:projectId/skills/import", "/api/v1/projects/:projectId/skills/refresh", "/api/v1/projects/:projectId/workflows", "/api/v1/projects/:projectId/statuses", "/api/v1/projects/:projectId/statuses/reset", "/api/v1/projects/:projectId/tickets", "/api/v1/projects/:projectId/tickets/archived", "/api/v1/projects/:projectId/tickets/:ticketId/detail", "/api/v1/projects/:projectId/tickets/:ticketId/repo-scopes", "/api/v1/projects/:projectId/tickets/:ticketId/runs", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId/raw-events", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId/activities", "/api/v1/projects/:projectId/tickets/:ticketId/runs/:runId/transcript", "/api/v1/projects/:projectId/repos", "/api/v1/projects/:projectId/token-usage", "/api/v1/projects/:projectId/github/namespaces", "/api/v1/projects/:projectId/github/repos", "/api/v1/projects/:projectId/agents", "/api/v1/projects/:projectId/agent-runs", "/api/v1/projects/:projectId/agents/:agentId/output", "/api/v1/projects/:projectId/agents/:agentId/output/stream", "/api/v1/projects/:projectId/agents/:agentId/steps", "/api/v1/projects/:projectId/agents/:agentId/steps/stream", "/api/v1/projects/:projectId/security-settings", "/api/v1/projects/:projectId/security-settings/api-keys", "/api/v1/projects/:projectId/security-settings/secrets", "/api/v1/projects/:projectId/security-settings/secrets/resolve-for-runtime", "/api/v1/projects/:projectId/security-settings/secret-bindings", "/api/v1/projects/:projectId/security-settings/github-outbound-credential", "/api/v1/projects/:projectId/security-settings/github-outbound-credential/import-gh-cli", "/api/v1/projects/:projectId/security-settings/github-outbound-credential/retest", "/api/v1/projects/:projectId/hr-advisor", "/api/v1/projects/:projectId/hr-advisor/activate", "/api/v1/projects/:projectId/role-bindings", "/api/v1/projects/:projectId/role-bindings/:bindingId", "/api/v1/chat/projects/:projectId/conversations/stream": return humanRouteAuthorizationRule{ scopeResolver: humanRouteScopeResolverProject, resource: "project", @@ -249,7 +249,7 @@ func humanRouteAuthorizationRuleFor(path string, method string) (humanRouteAutho permission: projectPermissionForPath(path, method), checkRequired: true, }, true - case "/api/v1/projects/:projectId/security-settings/secrets/:secretId", "/api/v1/projects/:projectId/security-settings/secrets/:secretId/rotate", "/api/v1/projects/:projectId/security-settings/secrets/:secretId/disable", "/api/v1/projects/:projectId/security-settings/secret-bindings/:bindingId": + case "/api/v1/projects/:projectId/security-settings/api-keys/:keyId", "/api/v1/projects/:projectId/security-settings/api-keys/:keyId/rotate", "/api/v1/projects/:projectId/security-settings/api-keys/:keyId/disable", "/api/v1/projects/:projectId/security-settings/secrets/:secretId", "/api/v1/projects/:projectId/security-settings/secrets/:secretId/rotate", "/api/v1/projects/:projectId/security-settings/secrets/:secretId/disable", "/api/v1/projects/:projectId/security-settings/secret-bindings/:bindingId": return humanRouteAuthorizationRule{ scopeResolver: humanRouteScopeResolverProject, resource: "project", diff --git a/internal/httpapi/openapi.go b/internal/httpapi/openapi.go index c8c4dabac..98fe4198e 100644 --- a/internal/httpapi/openapi.go +++ b/internal/httpapi/openapi.go @@ -2019,6 +2019,46 @@ type OpenAPISecurityAgentTokens struct { SupportedScopeGroups []OpenAPISecurityScopeGroup `json:"supported_scope_groups"` } +type OpenAPIProjectUserAPIKey struct { + ID string `json:"id"` + Name string `json:"name"` + TokenHint string `json:"token_hint"` + Scopes []string `json:"scopes"` + Status string `json:"status"` + ExpiresAt *string `json:"expires_at,omitempty"` + LastUsedAt *string `json:"last_used_at,omitempty"` + CreatedAt string `json:"created_at"` + DisabledAt *string `json:"disabled_at,omitempty"` + RevokedAt *string `json:"revoked_at,omitempty"` +} + +type OpenAPIProjectUserAPIKeysResponse struct { + APIKeys []OpenAPIProjectUserAPIKey `json:"api_keys"` +} + +type OpenAPIProjectUserAPIKeyResponse struct { + APIKey OpenAPIProjectUserAPIKey `json:"api_key"` +} + +type OpenAPIProjectUserAPIKeyCreateResponse struct { + APIKey OpenAPIProjectUserAPIKey `json:"api_key"` + PlainTextToken string `json:"plain_text_token"` +} + +type OpenAPICreateProjectUserAPIKeyRequest struct { + Name string `json:"name"` + Scopes []string `json:"scopes"` + ExpiresAt *string `json:"expires_at,omitempty"` +} + +type OpenAPISecurityUserAPIKeys struct { + TokenPrefix string `json:"token_prefix"` + SupportedScopes []string `json:"supported_scopes"` + SupportedScopeGroups []OpenAPISecurityScopeGroup `json:"supported_scope_groups"` + AllowedScopes []string `json:"allowed_scopes"` + AllowedScopeGroups []OpenAPISecurityScopeGroup `json:"allowed_scope_groups"` +} + type OpenAPISecurityAuthBootstrapState struct { Status string `json:"status"` AdminEmails []string `json:"admin_emails"` @@ -2133,6 +2173,7 @@ type OpenAPISecuritySettings struct { ProjectID string `json:"project_id"` Auth OpenAPISecurityAuthSettings `json:"auth"` AgentTokens OpenAPISecurityAgentTokens `json:"agent_tokens"` + UserAPIKeys OpenAPISecurityUserAPIKeys `json:"user_api_keys"` GitHub OpenAPIGitHubOutboundCredential `json:"github"` Webhooks OpenAPISecurityWebhooks `json:"webhooks"` SecretHygiene OpenAPISecuritySecretHygiene `json:"secret_hygiene"` @@ -6384,6 +6425,103 @@ func (b openAPISpecBuilder) addSecurityOperations() error { securityGet.AddParameter(uuidPathParameter("projectId", "Project ID.")) b.doc.AddOperation("/api/v1/projects/{projectId}/security-settings", http.MethodGet, securityGet) + userAPIKeyList, err := b.jsonOperation( + "listProjectUserAPIKeys", + "List the current user's project-scoped API keys", + []string{"security-settings"}, + http.StatusOK, + OpenAPIProjectUserAPIKeysResponse{}, + nil, + http.StatusBadRequest, + http.StatusUnauthorized, + http.StatusNotFound, + http.StatusServiceUnavailable, + http.StatusInternalServerError, + ) + if err != nil { + return err + } + userAPIKeyList.AddParameter(uuidPathParameter("projectId", "Project ID.")) + b.doc.AddOperation("/api/v1/projects/{projectId}/security-settings/api-keys", http.MethodGet, userAPIKeyList) + + userAPIKeyCreate, err := b.jsonOperation( + "createProjectUserAPIKey", + "Create a new project-scoped user API key for the current human principal", + []string{"security-settings"}, + http.StatusCreated, + OpenAPIProjectUserAPIKeyCreateResponse{}, + OpenAPICreateProjectUserAPIKeyRequest{}, + http.StatusBadRequest, + http.StatusUnauthorized, + http.StatusForbidden, + http.StatusNotFound, + http.StatusServiceUnavailable, + http.StatusInternalServerError, + ) + if err != nil { + return err + } + userAPIKeyCreate.AddParameter(uuidPathParameter("projectId", "Project ID.")) + b.doc.AddOperation("/api/v1/projects/{projectId}/security-settings/api-keys", http.MethodPost, userAPIKeyCreate) + + userAPIKeyRotate, err := b.jsonOperation( + "rotateProjectUserAPIKey", + "Rotate a project-scoped user API key and return the new plaintext token once", + []string{"security-settings"}, + http.StatusOK, + OpenAPIProjectUserAPIKeyCreateResponse{}, + nil, + http.StatusBadRequest, + http.StatusUnauthorized, + http.StatusForbidden, + http.StatusNotFound, + http.StatusServiceUnavailable, + http.StatusInternalServerError, + ) + if err != nil { + return err + } + userAPIKeyRotate.AddParameter(uuidPathParameter("projectId", "Project ID.")) + userAPIKeyRotate.AddParameter(uuidPathParameter("keyId", "API key ID.")) + b.doc.AddOperation("/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/rotate", http.MethodPost, userAPIKeyRotate) + + userAPIKeyDisable, err := b.jsonOperation( + "disableProjectUserAPIKey", + "Disable a project-scoped user API key immediately", + []string{"security-settings"}, + http.StatusOK, + OpenAPIProjectUserAPIKeyResponse{}, + nil, + http.StatusBadRequest, + http.StatusUnauthorized, + http.StatusForbidden, + http.StatusNotFound, + http.StatusServiceUnavailable, + http.StatusInternalServerError, + ) + if err != nil { + return err + } + userAPIKeyDisable.AddParameter(uuidPathParameter("projectId", "Project ID.")) + userAPIKeyDisable.AddParameter(uuidPathParameter("keyId", "API key ID.")) + b.doc.AddOperation("/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/disable", http.MethodPost, userAPIKeyDisable) + + userAPIKeyDelete := openapi3.NewOperation() + userAPIKeyDelete.OperationID = "deleteProjectUserAPIKey" + userAPIKeyDelete.Summary = "Delete a project-scoped user API key" + userAPIKeyDelete.Tags = []string{"security-settings"} + userAPIKeyDelete.AddResponse(http.StatusNoContent, openapi3.NewResponse().WithDescription("User API key deleted.")) + for _, code := range []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound, http.StatusServiceUnavailable, http.StatusInternalServerError} { + errorResponse, err := b.errorResponse(code) + if err != nil { + return err + } + userAPIKeyDelete.AddResponse(code, errorResponse) + } + userAPIKeyDelete.AddParameter(uuidPathParameter("projectId", "Project ID.")) + userAPIKeyDelete.AddParameter(uuidPathParameter("keyId", "API key ID.")) + b.doc.AddOperation("/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}", http.MethodDelete, userAPIKeyDelete) + secretList, err := b.jsonOperation( "listScopedSecrets", "List organization and project scoped secrets that are accessible from this project", diff --git a/internal/httpapi/security_settings_api.go b/internal/httpapi/security_settings_api.go index ac82f335e..d6116c091 100644 --- a/internal/httpapi/security_settings_api.go +++ b/internal/httpapi/security_settings_api.go @@ -7,13 +7,18 @@ import ( "slices" "time" + activitysvc "github.com/BetterAndBetterII/openase/internal/activity" "github.com/BetterAndBetterII/openase/internal/agentplatform" + activityevent "github.com/BetterAndBetterII/openase/internal/domain/activityevent" catalogdomain "github.com/BetterAndBetterII/openase/internal/domain/catalog" githubauthdomain "github.com/BetterAndBetterII/openase/internal/domain/githubauth" + humanauthdomain "github.com/BetterAndBetterII/openase/internal/domain/humanauth" iam "github.com/BetterAndBetterII/openase/internal/domain/iam" + userapikeydomain "github.com/BetterAndBetterII/openase/internal/domain/userapikey" accesscontrolservice "github.com/BetterAndBetterII/openase/internal/service/accesscontrol" githubauthservice "github.com/BetterAndBetterII/openase/internal/service/githubauth" humanauthservice "github.com/BetterAndBetterII/openase/internal/service/humanauth" + userapikeyservice "github.com/BetterAndBetterII/openase/internal/service/userapikey" "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -40,6 +45,27 @@ type securityAgentTokensResponse struct { SupportedScopeGroups []securityScopeGroupResponse `json:"supported_scope_groups"` } +type securityUserAPIKeysResponse struct { + TokenPrefix string `json:"token_prefix"` + SupportedScopes []string `json:"supported_scopes"` + SupportedScopeGroups []securityScopeGroupResponse `json:"supported_scope_groups"` + AllowedScopes []string `json:"allowed_scopes"` + AllowedScopeGroups []securityScopeGroupResponse `json:"allowed_scope_groups"` +} + +type projectUserAPIKeyResponse struct { + ID string `json:"id"` + Name string `json:"name"` + TokenHint string `json:"token_hint"` + Scopes []string `json:"scopes"` + Status string `json:"status"` + ExpiresAt *string `json:"expires_at,omitempty"` + LastUsedAt *string `json:"last_used_at,omitempty"` + CreatedAt string `json:"created_at"` + DisabledAt *string `json:"disabled_at,omitempty"` + RevokedAt *string `json:"revoked_at,omitempty"` +} + type securityScopeGroupResponse struct { Category string `json:"category"` Scopes []string `json:"scopes"` @@ -102,6 +128,7 @@ type securitySettingsResponse struct { ProjectID string `json:"project_id"` Auth securityAuthSettingsResponse `json:"auth"` AgentTokens securityAgentTokensResponse `json:"agent_tokens"` + UserAPIKeys securityUserAPIKeysResponse `json:"user_api_keys"` GitHub securityGitHubOutboundCredentialResponse `json:"github"` Webhooks securityWebhookBoundaryResponse `json:"webhooks"` SecretHygiene securitySecretHygieneResponse `json:"secret_hygiene"` @@ -116,6 +143,11 @@ func (s *Server) registerSecuritySettingsRoutes(api *echo.Group) { api.POST("/orgs/:orgId/security-settings/secrets/:secretId/disable", s.handleDisableOrganizationScopedSecret) api.DELETE("/orgs/:orgId/security-settings/secrets/:secretId", s.handleDeleteOrganizationScopedSecret) api.GET("/projects/:projectId/security-settings", s.handleGetSecuritySettings) + api.GET("/projects/:projectId/security-settings/api-keys", s.handleListProjectUserAPIKeys) + api.POST("/projects/:projectId/security-settings/api-keys", s.handleCreateProjectUserAPIKey) + api.POST("/projects/:projectId/security-settings/api-keys/:keyId/rotate", s.handleRotateProjectUserAPIKey) + api.POST("/projects/:projectId/security-settings/api-keys/:keyId/disable", s.handleDisableProjectUserAPIKey) + api.DELETE("/projects/:projectId/security-settings/api-keys/:keyId", s.handleDeleteProjectUserAPIKey) api.GET("/projects/:projectId/security-settings/secrets", s.handleListScopedSecrets) api.POST("/projects/:projectId/security-settings/secrets", s.handleCreateScopedSecret) api.GET("/projects/:projectId/security-settings/secret-bindings", s.handleListScopedSecretBindings) @@ -153,6 +185,114 @@ func (s *Server) handleGetSecuritySettings(c echo.Context) error { return s.writeSecuritySettingsResponse(c, projectID, githubSecurity) } +func (s *Server) handleListProjectUserAPIKeys(c echo.Context) error { + projectID, principal, err := s.requireProjectUserAPIKeyContext(c) + if err != nil { + return err + } + if s.userAPIKeyService == nil { + return writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", userapikeyservice.ErrUnavailable.Error()) + } + items, err := s.userAPIKeyService.List(c.Request().Context(), projectID, principal) + if err != nil { + return writeProjectUserAPIKeyError(c, err) + } + return c.JSON(http.StatusOK, map[string]any{ + "api_keys": mapProjectUserAPIKeyResponses(items), + }) +} + +func (s *Server) handleCreateProjectUserAPIKey(c echo.Context) error { + projectID, principal, err := s.requireProjectUserAPIKeyContext(c) + if err != nil { + return err + } + if s.userAPIKeyService == nil { + return writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", userapikeyservice.ErrUnavailable.Error()) + } + var raw rawCreateProjectUserAPIKeyRequest + if err := decodeJSON(c, &raw); err != nil { + return err + } + input, err := parseCreateProjectUserAPIKeyRequest(projectID, principal.User.ID, raw) + if err != nil { + return writeAPIError(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error()) + } + result, err := s.userAPIKeyService.Create(c.Request().Context(), principal, input) + if err != nil { + return writeProjectUserAPIKeyError(c, err) + } + s.emitProjectUserAPIKeyActivity(c.Request().Context(), projectID, principal, activityevent.TypeSecretCreated, "Created user API key", result.APIKey) + return c.JSON(http.StatusCreated, map[string]any{ + "api_key": mapProjectUserAPIKeyResponse(result.APIKey), + "plain_text_token": result.PlainTextToken, + }) +} + +func (s *Server) handleRotateProjectUserAPIKey(c echo.Context) error { + projectID, principal, err := s.requireProjectUserAPIKeyContext(c) + if err != nil { + return err + } + if s.userAPIKeyService == nil { + return writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", userapikeyservice.ErrUnavailable.Error()) + } + keyID, err := parseUUIDPathParamValue(c, "keyId") + if err != nil { + return writeAPIError(c, http.StatusBadRequest, "INVALID_API_KEY_ID", err.Error()) + } + result, err := s.userAPIKeyService.Rotate(c.Request().Context(), projectID, keyID, principal) + if err != nil { + return writeProjectUserAPIKeyError(c, err) + } + s.emitProjectUserAPIKeyActivity(c.Request().Context(), projectID, principal, activityevent.TypeSecretRotated, "Rotated user API key", result.APIKey) + return c.JSON(http.StatusOK, map[string]any{ + "api_key": mapProjectUserAPIKeyResponse(result.APIKey), + "plain_text_token": result.PlainTextToken, + }) +} + +func (s *Server) handleDisableProjectUserAPIKey(c echo.Context) error { + projectID, principal, err := s.requireProjectUserAPIKeyContext(c) + if err != nil { + return err + } + if s.userAPIKeyService == nil { + return writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", userapikeyservice.ErrUnavailable.Error()) + } + keyID, err := parseUUIDPathParamValue(c, "keyId") + if err != nil { + return writeAPIError(c, http.StatusBadRequest, "INVALID_API_KEY_ID", err.Error()) + } + item, err := s.userAPIKeyService.Disable(c.Request().Context(), projectID, keyID, principal) + if err != nil { + return writeProjectUserAPIKeyError(c, err) + } + s.emitProjectUserAPIKeyActivity(c.Request().Context(), projectID, principal, activityevent.TypeSecretDisabled, "Disabled user API key", item) + return c.JSON(http.StatusOK, map[string]any{ + "api_key": mapProjectUserAPIKeyResponse(item), + }) +} + +func (s *Server) handleDeleteProjectUserAPIKey(c echo.Context) error { + projectID, principal, err := s.requireProjectUserAPIKeyContext(c) + if err != nil { + return err + } + if s.userAPIKeyService == nil { + return writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", userapikeyservice.ErrUnavailable.Error()) + } + keyID, err := parseUUIDPathParamValue(c, "keyId") + if err != nil { + return writeAPIError(c, http.StatusBadRequest, "INVALID_API_KEY_ID", err.Error()) + } + if err := s.userAPIKeyService.Delete(c.Request().Context(), projectID, keyID, principal); err != nil { + return writeProjectUserAPIKeyError(c, err) + } + s.emitProjectUserAPIKeyDeleteActivity(c.Request().Context(), projectID, principal, keyID) + return c.NoContent(http.StatusNoContent) +} + func (s *Server) handlePutGitHubOutboundCredential(c echo.Context) error { projectID, err := s.requireProjectSecurityContext(c) if err != nil { @@ -215,6 +355,7 @@ func (s *Server) handlePutOIDCDraft(c echo.Context) error { projectID, stored, s.readGitHubSecurity(c, projectID), + securityUserAPIKeysResponse{}, s.buildSecretHygieneResponse(c.Request().Context(), projectID), s.resolveApprovalPoliciesSummary(c), s.cfg.Host, @@ -353,6 +494,7 @@ func (s *Server) handleEnableOIDC(c echo.Context) error { projectID, stored, githubSecurity, + securityUserAPIKeysResponse{}, s.buildSecretHygieneResponse(c.Request().Context(), projectID), s.resolveApprovalPoliciesSummary(c), s.cfg.Host, @@ -423,6 +565,20 @@ func (s *Server) requireProjectSecurityContext(c echo.Context) (uuid.UUID, error return projectID, nil } +func (s *Server) requireProjectUserAPIKeyContext( + c echo.Context, +) (uuid.UUID, humanauthdomain.AuthenticatedPrincipal, error) { + projectID, err := s.requireProjectSecurityContext(c) + if err != nil { + return uuid.UUID{}, humanauthdomain.AuthenticatedPrincipal{}, err + } + principal, ok := currentHumanPrincipal(c) + if !ok { + return uuid.UUID{}, humanauthdomain.AuthenticatedPrincipal{}, writeAPIError(c, http.StatusUnauthorized, "HUMAN_SESSION_REQUIRED", "human session required") + } + return projectID, principal, nil +} + func (s *Server) requireOrganizationSecurityContext(c echo.Context) (uuid.UUID, error) { if s.catalog.Empty() { return uuid.UUID{}, writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", "catalog service unavailable") @@ -447,11 +603,32 @@ func (s *Server) writeSecuritySettingsResponse( if err != nil { return writeAPIError(c, http.StatusInternalServerError, "SECURITY_SETTINGS_CONFIG_FAILED", err.Error()) } + userAPIKeys := securityUserAPIKeysResponse{ + TokenPrefix: agentplatform.UserAPIKeyTokenPrefix(), + SupportedScopes: slices.Clone(agentplatform.SupportedScopesForPrincipalKind(agentplatform.PrincipalKindUserAPIKey)), + SupportedScopeGroups: mapSecurityScopeGroups(agentplatform.SupportedScopeGroupsForPrincipalKind(agentplatform.PrincipalKindUserAPIKey)), + } + if s.userAPIKeyService != nil { + if principal, ok := currentHumanPrincipal(c); ok { + allowedScopes, allowedGroups, err := s.userAPIKeyService.AllowedScopes(c.Request().Context(), projectID, &principal) + if err == nil { + userAPIKeys.AllowedScopes = allowedScopes + userAPIKeys.AllowedScopeGroups = mapSecurityScopeGroups(allowedGroups) + } + } else { + allowedScopes, allowedGroups, err := s.userAPIKeyService.AllowedScopes(c.Request().Context(), projectID, nil) + if err == nil { + userAPIKeys.AllowedScopes = allowedScopes + userAPIKeys.AllowedScopeGroups = mapSecurityScopeGroups(allowedGroups) + } + } + } return c.JSON(http.StatusOK, map[string]any{ "security": buildSecuritySettingsResponse( projectID, stored, github, + userAPIKeys, s.buildSecretHygieneResponse(c.Request().Context(), projectID), s.resolveApprovalPoliciesSummary(c), s.cfg.Host, @@ -563,11 +740,19 @@ func buildSecuritySettingsResponse( projectID uuid.UUID, stored accesscontrolservice.ReadResult, github securityGitHubOutboundCredentialResponse, + userAPIKeys securityUserAPIKeysResponse, secretHygiene securitySecretHygieneResponse, approvalPolicies securityApprovalPoliciesResponse, host string, ) securitySettingsResponse { runtimeState := iam.ResolveRuntimeAccessControlState(stored.State) + if userAPIKeys.TokenPrefix == "" { + userAPIKeys = securityUserAPIKeysResponse{ + TokenPrefix: agentplatform.UserAPIKeyTokenPrefix(), + SupportedScopes: slices.Clone(agentplatform.SupportedScopesForPrincipalKind(agentplatform.PrincipalKindUserAPIKey)), + SupportedScopeGroups: mapSecurityScopeGroups(agentplatform.SupportedScopeGroupsForPrincipalKind(agentplatform.PrincipalKindUserAPIKey)), + } + } return securitySettingsResponse{ ProjectID: projectID.String(), Auth: buildSecurityAuthSettingsResponseFromAccessControl(runtimeState, stored.State, stored.StorageLocation, host), @@ -579,7 +764,8 @@ func buildSecuritySettingsResponse( SupportedProjectScopes: slices.Clone(agentplatform.SupportedScopesForPrincipalKind(agentplatform.PrincipalKindProjectConversation)), SupportedScopeGroups: mapSecurityScopeGroups(agentplatform.SupportedScopeGroupsForPrincipalKind(agentplatform.PrincipalKindProjectConversation)), }, - GitHub: github, + UserAPIKeys: userAPIKeys, + GitHub: github, Webhooks: securityWebhookBoundaryResponse{ ConnectorEndpoint: securitySettingsConnectorEndpoint, }, @@ -632,6 +818,37 @@ func mapSecurityScopeGroups(items []agentplatform.ScopeGroup) []securityScopeGro return response } +func mapProjectUserAPIKeyResponses(items []userapikeydomain.APIKey) []projectUserAPIKeyResponse { + response := make([]projectUserAPIKeyResponse, 0, len(items)) + for _, item := range items { + response = append(response, mapProjectUserAPIKeyResponse(item)) + } + return response +} + +func mapProjectUserAPIKeyResponse(item userapikeydomain.APIKey) projectUserAPIKeyResponse { + return projectUserAPIKeyResponse{ + ID: item.ID.String(), + Name: item.Name, + TokenHint: item.TokenHint, + Scopes: append([]string(nil), item.Scopes...), + Status: string(item.Status), + ExpiresAt: formatOptionalSecurityTime(item.ExpiresAt), + LastUsedAt: formatOptionalSecurityTime(item.LastUsedAt), + CreatedAt: item.CreatedAt.UTC().Format(time.RFC3339), + DisabledAt: formatOptionalSecurityTime(item.DisabledAt), + RevokedAt: formatOptionalSecurityTime(item.RevokedAt), + } +} + +func formatOptionalSecurityTime(value *time.Time) *string { + if value == nil { + return nil + } + formatted := value.UTC().Format(time.RFC3339) + return &formatted +} + func buildMissingGitHubSecurityResponse() securityGitHubOutboundCredentialResponse { return securityGitHubOutboundCredentialResponse{ Effective: buildMissingGitHubCredentialSlot(""), @@ -640,6 +857,68 @@ func buildMissingGitHubSecurityResponse() securityGitHubOutboundCredentialRespon } } +func writeProjectUserAPIKeyError(c echo.Context, err error) error { + switch { + case errors.Is(err, userapikeyservice.ErrUnavailable): + return writeAPIError(c, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", err.Error()) + case errors.Is(err, userapikeyservice.ErrInvalidInput): + return writeAPIError(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error()) + case errors.Is(err, userapikeyservice.ErrForbidden): + return writeAPIError(c, http.StatusForbidden, "USER_API_KEY_FORBIDDEN", err.Error()) + case errors.Is(err, userapikeyservice.ErrScopeForbidden): + return writeAPIError(c, http.StatusForbidden, "USER_API_KEY_SCOPE_FORBIDDEN", "requested scopes exceed your current project permissions") + case errors.Is(err, userapikeyservice.ErrNotFound): + return writeAPIError(c, http.StatusNotFound, "USER_API_KEY_NOT_FOUND", err.Error()) + default: + return writeAPIError(c, http.StatusInternalServerError, "USER_API_KEY_OPERATION_FAILED", err.Error()) + } +} + +func (s *Server) emitProjectUserAPIKeyActivity( + ctx context.Context, + projectID uuid.UUID, + principal humanauthdomain.AuthenticatedPrincipal, + eventType activityevent.Type, + message string, + item userapikeydomain.APIKey, +) { + if s == nil || s.activityEmitter == nil { + return + } + _, _ = s.activityEmitter.Emit(ctx, activitysvc.RecordInput{ + ProjectID: projectID, + EventType: eventType, + Message: message, + Metadata: map[string]any{ + "audit_actor": principal.ActorID(), + "api_key_id": item.ID.String(), + "api_key_name": item.Name, + "scopes": append([]string(nil), item.Scopes...), + "status": string(item.Status), + }, + }) +} + +func (s *Server) emitProjectUserAPIKeyDeleteActivity( + ctx context.Context, + projectID uuid.UUID, + principal humanauthdomain.AuthenticatedPrincipal, + keyID uuid.UUID, +) { + if s == nil || s.activityEmitter == nil { + return + } + _, _ = s.activityEmitter.Emit(ctx, activitysvc.RecordInput{ + ProjectID: projectID, + EventType: activityevent.TypeSecretDeleted, + Message: "Deleted user API key", + Metadata: map[string]any{ + "audit_actor": principal.ActorID(), + "api_key_id": keyID.String(), + }, + }) +} + func buildMissingGitHubCredentialSlot(scope string) securityGitHubCredentialSlotResponse { return securityGitHubCredentialSlotResponse{ Scope: scope, diff --git a/internal/httpapi/security_settings_requests.go b/internal/httpapi/security_settings_requests.go index 920773d6d..0e9e2645f 100644 --- a/internal/httpapi/security_settings_requests.go +++ b/internal/httpapi/security_settings_requests.go @@ -5,6 +5,7 @@ import ( "strings" githubauthdomain "github.com/BetterAndBetterII/openase/internal/domain/githubauth" + userapikeydomain "github.com/BetterAndBetterII/openase/internal/domain/userapikey" githubauthservice "github.com/BetterAndBetterII/openase/internal/service/githubauth" "github.com/google/uuid" ) @@ -27,6 +28,12 @@ type rawSecurityOIDCDraftRequest struct { SessionIdleTTL string `json:"session_idle_ttl,omitempty"` } +type rawCreateProjectUserAPIKeyRequest struct { + Name string `json:"name"` + Scopes []string `json:"scopes"` + ExpiresAt *string `json:"expires_at,omitempty"` +} + func parseSaveGitHubOutboundCredentialRequest( projectID uuid.UUID, raw rawSaveGitHubOutboundCredentialRequest, @@ -48,3 +55,17 @@ func parseGitHubCredentialScopeRequest(projectID uuid.UUID) githubauthservice.Sc Scope: githubauthdomain.ScopeProject, } } + +func parseCreateProjectUserAPIKeyRequest( + projectID uuid.UUID, + userID uuid.UUID, + raw rawCreateProjectUserAPIKeyRequest, +) (userapikeydomain.CreateInput, error) { + return userapikeydomain.ParseCreate(userapikeydomain.ParseCreateInput{ + ProjectID: projectID.String(), + UserID: userID, + Name: raw.Name, + Scopes: raw.Scopes, + ExpiresAt: raw.ExpiresAt, + }) +} diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 0d6ee3c40..efe79c22a 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -34,6 +34,7 @@ import ( githubreposervice "github.com/BetterAndBetterII/openase/internal/service/githubrepo" humanauthservice "github.com/BetterAndBetterII/openase/internal/service/humanauth" secretsservice "github.com/BetterAndBetterII/openase/internal/service/secrets" + userapikeyservice "github.com/BetterAndBetterII/openase/internal/service/userapikey" ticketservice "github.com/BetterAndBetterII/openase/internal/ticket" "github.com/BetterAndBetterII/openase/internal/ticketstatus" workflowservice "github.com/BetterAndBetterII/openase/internal/workflow" @@ -70,6 +71,7 @@ type Server struct { githubAuthService githubauthservice.SecurityManager githubRepoService githubreposervice.Service secretService secretsservice.Manager + userAPIKeyService *userapikeyservice.Service instanceAuthService *accesscontrolservice.Service humanAuthService *humanauthservice.Service humanAuthorizer *humanauthservice.Authorizer @@ -140,6 +142,12 @@ func WithSecretService(service secretsservice.Manager) ServerOption { } } +func WithUserAPIKeyService(service *userapikeyservice.Service) ServerOption { + return func(server *Server) { + server.userAPIKeyService = service + } +} + func WithHumanAuthConfig(cfg config.AuthConfig) ServerOption { return func(server *Server) { server.auth = cfg diff --git a/internal/repo/agentplatform/repo.go b/internal/repo/agentplatform/repo.go index 97a95fa5b..b06ca3d4f 100644 --- a/internal/repo/agentplatform/repo.go +++ b/internal/repo/agentplatform/repo.go @@ -10,6 +10,7 @@ import ( entagent "github.com/BetterAndBetterII/openase/ent/agent" entagenttoken "github.com/BetterAndBetterII/openase/ent/agenttoken" entprojectconversationprincipal "github.com/BetterAndBetterII/openase/ent/projectconversationprincipal" + entuserapikey "github.com/BetterAndBetterII/openase/ent/userapikey" domain "github.com/BetterAndBetterII/openase/internal/domain/agentplatform" "github.com/google/uuid" ) @@ -112,6 +113,35 @@ func (r *EntRepository) TouchTokenLastUsed(ctx context.Context, tokenID uuid.UUI return err } +func (r *EntRepository) UserAPIKeyByHash(ctx context.Context, tokenHash string) (domain.StoredUserAPIKeyRecord, error) { + record, err := r.client.UserAPIKey.Query(). + Where(entuserapikey.TokenHashEQ(tokenHash)). + Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return domain.StoredUserAPIKeyRecord{}, fmt.Errorf("%w: user api key", domain.ErrNotFound) + } + return domain.StoredUserAPIKeyRecord{}, err + } + return domain.StoredUserAPIKeyRecord{ + ID: record.ID, + UserID: record.UserID, + ProjectID: record.ProjectID, + Name: record.Name, + Scopes: copyStrings(record.Scopes), + Status: string(record.Status), + ExpiresAt: cloneTimePointer(record.ExpiresAt), + LastUsedAt: cloneTimePointer(record.LastUsedAt), + }, nil +} + +func (r *EntRepository) TouchUserAPIKeyLastUsed(ctx context.Context, keyID uuid.UUID, usedAt time.Time) error { + _, err := r.client.UserAPIKey.UpdateOneID(keyID). + SetLastUsedAt(usedAt). + Save(ctx) + return err +} + func (r *EntRepository) ProjectTokenInventory(ctx context.Context, projectID uuid.UUID, now time.Time) (domain.ProjectTokenInventory, error) { baseQuery := r.client.AgentToken.Query().Where(entagenttoken.ProjectIDEQ(projectID)) diff --git a/internal/repo/userapikey/repo.go b/internal/repo/userapikey/repo.go new file mode 100644 index 000000000..6fe6868a2 --- /dev/null +++ b/internal/repo/userapikey/repo.go @@ -0,0 +1,205 @@ +package userapikey + +import ( + "context" + "errors" + "time" + + "github.com/BetterAndBetterII/openase/ent" + entuserapikey "github.com/BetterAndBetterII/openase/ent/userapikey" + domain "github.com/BetterAndBetterII/openase/internal/domain/userapikey" + "github.com/google/uuid" +) + +type Repository struct { + client *ent.Client +} + +var ErrNotFound = errors.New("user api key not found") + +func NewEntRepository(client *ent.Client) *Repository { + return &Repository{client: client} +} + +type CreateRecord struct { + UserID uuid.UUID + ProjectID uuid.UUID + Name string + TokenPrefix string + TokenHint string + TokenHash string + Scopes []string + ExpiresAt *time.Time +} + +type RotateRecord struct { + Name string + TokenPrefix string + TokenHint string + TokenHash string + Scopes []string + ExpiresAt *time.Time + RotatedAt time.Time +} + +func (r *Repository) ListByProjectAndUser(ctx context.Context, projectID, userID uuid.UUID) ([]domain.APIKey, error) { + items, err := r.client.UserAPIKey.Query(). + Where( + entuserapikey.ProjectIDEQ(projectID), + entuserapikey.UserIDEQ(userID), + entuserapikey.StatusNEQ(entuserapikey.StatusRevoked), + ). + Order(ent.Desc(entuserapikey.FieldCreatedAt)). + All(ctx) + if err != nil { + return nil, err + } + result := make([]domain.APIKey, 0, len(items)) + for _, item := range items { + result = append(result, mapAPIKey(item)) + } + return result, nil +} + +func (r *Repository) Create(ctx context.Context, record CreateRecord) (domain.APIKey, error) { + builder := r.client.UserAPIKey.Create(). + SetUserID(record.UserID). + SetProjectID(record.ProjectID). + SetName(record.Name). + SetTokenPrefix(record.TokenPrefix). + SetTokenHint(record.TokenHint). + SetTokenHash(record.TokenHash). + SetScopes(copyStrings(record.Scopes)) + if record.ExpiresAt != nil { + builder.SetExpiresAt(record.ExpiresAt.UTC()) + } + item, err := builder.Save(ctx) + if err != nil { + return domain.APIKey{}, err + } + return mapAPIKey(item), nil +} + +func (r *Repository) GetByIDForUser(ctx context.Context, keyID, projectID, userID uuid.UUID) (domain.APIKey, error) { + item, err := r.client.UserAPIKey.Query(). + Where( + entuserapikey.IDEQ(keyID), + entuserapikey.ProjectIDEQ(projectID), + entuserapikey.UserIDEQ(userID), + ). + Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return domain.APIKey{}, ErrNotFound + } + return domain.APIKey{}, err + } + return mapAPIKey(item), nil +} + +func (r *Repository) Disable(ctx context.Context, keyID uuid.UUID, disabledAt time.Time) (domain.APIKey, error) { + item, err := r.client.UserAPIKey.UpdateOneID(keyID). + SetStatus(entuserapikey.StatusDisabled). + SetDisabledAt(disabledAt.UTC()). + Save(ctx) + if err != nil { + if ent.IsNotFound(err) { + return domain.APIKey{}, ErrNotFound + } + return domain.APIKey{}, err + } + return mapAPIKey(item), nil +} + +func (r *Repository) Revoke(ctx context.Context, keyID uuid.UUID, revokedAt time.Time) error { + _, err := r.client.UserAPIKey.UpdateOneID(keyID). + SetStatus(entuserapikey.StatusRevoked). + SetRevokedAt(revokedAt.UTC()). + Save(ctx) + if ent.IsNotFound(err) { + return ErrNotFound + } + return err +} + +func (r *Repository) Rotate(ctx context.Context, keyID uuid.UUID, record RotateRecord) (domain.APIKey, error) { + builder := r.client.UserAPIKey.UpdateOneID(keyID). + SetName(record.Name). + SetTokenPrefix(record.TokenPrefix). + SetTokenHint(record.TokenHint). + SetTokenHash(record.TokenHash). + SetScopes(copyStrings(record.Scopes)). + SetStatus(entuserapikey.StatusActive). + ClearDisabledAt(). + ClearRevokedAt() + if record.ExpiresAt != nil { + builder.SetExpiresAt(record.ExpiresAt.UTC()) + } else { + builder.ClearExpiresAt() + } + item, err := builder.Save(ctx) + if err != nil { + if ent.IsNotFound(err) { + return domain.APIKey{}, ErrNotFound + } + return domain.APIKey{}, err + } + return mapAPIKey(item), nil +} + +func (r *Repository) TokenByHash(ctx context.Context, tokenHash string) (domain.APIKey, error) { + item, err := r.client.UserAPIKey.Query(). + Where(entuserapikey.TokenHashEQ(tokenHash)). + Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return domain.APIKey{}, ErrNotFound + } + return domain.APIKey{}, err + } + return mapAPIKey(item), nil +} + +func (r *Repository) TouchLastUsed(ctx context.Context, keyID uuid.UUID, usedAt time.Time) error { + _, err := r.client.UserAPIKey.UpdateOneID(keyID). + SetLastUsedAt(usedAt.UTC()). + Save(ctx) + if ent.IsNotFound(err) { + return ErrNotFound + } + return err +} + +func mapAPIKey(item *ent.UserAPIKey) domain.APIKey { + return domain.APIKey{ + ID: item.ID, + UserID: item.UserID, + ProjectID: item.ProjectID, + Name: item.Name, + TokenPrefix: item.TokenPrefix, + TokenHint: item.TokenHint, + Scopes: copyStrings(item.Scopes), + Status: domain.Status(item.Status), + ExpiresAt: cloneTime(item.ExpiresAt), + LastUsedAt: cloneTime(item.LastUsedAt), + CreatedAt: item.CreatedAt.UTC(), + UpdatedAt: item.UpdatedAt.UTC(), + DisabledAt: cloneTime(item.DisabledAt), + RevokedAt: cloneTime(item.RevokedAt), + } +} + +func cloneTime(value *time.Time) *time.Time { + if value == nil { + return nil + } + utc := value.UTC() + return &utc +} + +func copyStrings(items []string) []string { + if len(items) == 0 { + return nil + } + return append([]string(nil), items...) +} diff --git a/internal/service/userapikey/service.go b/internal/service/userapikey/service.go new file mode 100644 index 000000000..5d826d58b --- /dev/null +++ b/internal/service/userapikey/service.go @@ -0,0 +1,266 @@ +package userapikey + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "io" + "slices" + "strings" + "time" + + "github.com/BetterAndBetterII/openase/internal/agentplatform" + agentplatformdomain "github.com/BetterAndBetterII/openase/internal/domain/agentplatform" + humanauthdomain "github.com/BetterAndBetterII/openase/internal/domain/humanauth" + domain "github.com/BetterAndBetterII/openase/internal/domain/userapikey" + userapikeyrepo "github.com/BetterAndBetterII/openase/internal/repo/userapikey" + humanauthservice "github.com/BetterAndBetterII/openase/internal/service/humanauth" + "github.com/google/uuid" +) + +var ( + ErrUnavailable = errors.New("user api key service unavailable") + ErrInvalidInput = errors.New("user api key input is invalid") + ErrNotFound = errors.New("user api key not found") + ErrForbidden = errors.New("user api key access is forbidden") + ErrScopeForbidden = errors.New("user api key scope is forbidden") +) + +type Service struct { + repo *userapikeyrepo.Repository + authorizer *humanauthservice.Authorizer + now func() time.Time + rng io.Reader +} + +func NewService(repo *userapikeyrepo.Repository, authorizer *humanauthservice.Authorizer) *Service { + return &Service{repo: repo, authorizer: authorizer, now: time.Now, rng: rand.Reader} +} + +func (s *Service) List(ctx context.Context, projectID uuid.UUID, principal humanauthdomain.AuthenticatedPrincipal) ([]domain.APIKey, error) { + if s == nil || s.repo == nil { + return nil, ErrUnavailable + } + if projectID == uuid.Nil { + return nil, fmt.Errorf("%w: project_id must be a valid UUID", ErrInvalidInput) + } + if principal.User.ID == uuid.Nil { + return nil, fmt.Errorf("%w: principal user is required", ErrForbidden) + } + return s.repo.ListByProjectAndUser(ctx, projectID, principal.User.ID) +} + +func (s *Service) AllowedScopes(ctx context.Context, projectID uuid.UUID, principal *humanauthdomain.AuthenticatedPrincipal) ([]string, []agentplatformdomain.ScopeGroup, error) { + requestedSupported := agentplatform.SupportedScopesForPrincipalKind(agentplatform.PrincipalKindUserAPIKey) + if principal == nil { + return requestedSupported, domain.SupportedScopeGroups(requestedSupported), nil + } + if s == nil || s.authorizer == nil { + return nil, nil, ErrUnavailable + } + if projectID == uuid.Nil { + return nil, nil, fmt.Errorf("%w: project_id must be a valid UUID", ErrInvalidInput) + } + roles, permissions, err := s.authorizer.Evaluate( + ctx, + principal.User, + principal.Identity, + principal.Groups, + humanauthdomain.ScopeRef{Kind: humanauthdomain.ScopeKindProject, ID: projectID.String()}, + ) + _ = roles + if err != nil { + return nil, nil, err + } + allowed := scopesForPermissions(permissions) + allowed = intersectStrings(allowed, requestedSupported) + return allowed, domain.SupportedScopeGroups(allowed), nil +} + +func (s *Service) Create(ctx context.Context, principal humanauthdomain.AuthenticatedPrincipal, input domain.CreateInput) (domain.CreateResult, error) { + if s == nil || s.repo == nil { + return domain.CreateResult{}, ErrUnavailable + } + if principal.User.ID == uuid.Nil || principal.User.ID != input.UserID { + return domain.CreateResult{}, ErrForbidden + } + allowedScopes, _, err := s.AllowedScopes(ctx, input.ProjectID, &principal) + if err != nil { + return domain.CreateResult{}, err + } + requested, err := agentplatform.ParseExplicitScopesForPrincipalKind(agentplatform.PrincipalKindUserAPIKey, input.Scopes) + if err != nil { + return domain.CreateResult{}, fmt.Errorf("%w: %v", ErrInvalidInput, err) + } + if len(requested) == 0 { + return domain.CreateResult{}, fmt.Errorf("%w: at least one scope must be selected", ErrInvalidInput) + } + if !isSubset(requested, allowedScopes) { + return domain.CreateResult{}, ErrScopeForbidden + } + if input.ExpiresAt != nil && !input.ExpiresAt.After(s.now().UTC()) { + return domain.CreateResult{}, fmt.Errorf("%w: expires_at must be in the future", ErrInvalidInput) + } + plainTextToken, tokenHash, err := agentplatform.GenerateOpaqueToken(agentplatform.UserAPIKeyTokenPrefix(), s.rng) + if err != nil { + return domain.CreateResult{}, err + } + created, err := s.repo.Create(ctx, userapikeyrepo.CreateRecord{ + UserID: input.UserID, + ProjectID: input.ProjectID, + Name: input.Name, + TokenPrefix: agentplatform.UserAPIKeyTokenPrefix(), + TokenHint: agentplatform.TokenPreview(plainTextToken), + TokenHash: tokenHash, + Scopes: requested, + ExpiresAt: input.ExpiresAt, + }) + if err != nil { + return domain.CreateResult{}, err + } + return domain.CreateResult{APIKey: created, PlainTextToken: plainTextToken}, nil +} + +func (s *Service) Disable(ctx context.Context, projectID, keyID uuid.UUID, principal humanauthdomain.AuthenticatedPrincipal) (domain.APIKey, error) { + item, err := s.requireOwnedKey(ctx, projectID, keyID, principal) + if err != nil { + return domain.APIKey{}, err + } + if item.Status == domain.StatusRevoked { + return domain.APIKey{}, ErrNotFound + } + if item.Status == domain.StatusDisabled { + return item, nil + } + return s.repo.Disable(ctx, keyID, s.now().UTC()) +} + +func (s *Service) Rotate(ctx context.Context, projectID, keyID uuid.UUID, principal humanauthdomain.AuthenticatedPrincipal) (domain.CreateResult, error) { + item, err := s.requireOwnedKey(ctx, projectID, keyID, principal) + if err != nil { + return domain.CreateResult{}, err + } + if item.Status == domain.StatusRevoked { + return domain.CreateResult{}, ErrNotFound + } + allowedScopes, _, err := s.AllowedScopes(ctx, projectID, &principal) + if err != nil { + return domain.CreateResult{}, err + } + if !isSubset(item.Scopes, allowedScopes) { + return domain.CreateResult{}, ErrScopeForbidden + } + plainTextToken, tokenHash, err := agentplatform.GenerateOpaqueToken(agentplatform.UserAPIKeyTokenPrefix(), s.rng) + if err != nil { + return domain.CreateResult{}, err + } + rotated, err := s.repo.Rotate(ctx, keyID, userapikeyrepo.RotateRecord{ + Name: item.Name, + TokenPrefix: agentplatform.UserAPIKeyTokenPrefix(), + TokenHint: agentplatform.TokenPreview(plainTextToken), + TokenHash: tokenHash, + Scopes: item.Scopes, + ExpiresAt: item.ExpiresAt, + RotatedAt: s.now().UTC(), + }) + if err != nil { + return domain.CreateResult{}, err + } + return domain.CreateResult{APIKey: rotated, PlainTextToken: plainTextToken}, nil +} + +func (s *Service) Delete(ctx context.Context, projectID, keyID uuid.UUID, principal humanauthdomain.AuthenticatedPrincipal) error { + item, err := s.requireOwnedKey(ctx, projectID, keyID, principal) + if err != nil { + return err + } + if item.Status == domain.StatusRevoked { + return nil + } + if err := s.repo.Revoke(ctx, keyID, s.now().UTC()); err != nil { + if errors.Is(err, userapikeyrepo.ErrNotFound) { + return ErrNotFound + } + return err + } + return nil +} + +func (s *Service) requireOwnedKey(ctx context.Context, projectID, keyID uuid.UUID, principal humanauthdomain.AuthenticatedPrincipal) (domain.APIKey, error) { + if s == nil || s.repo == nil { + return domain.APIKey{}, ErrUnavailable + } + if projectID == uuid.Nil || keyID == uuid.Nil || principal.User.ID == uuid.Nil { + return domain.APIKey{}, ErrInvalidInput + } + item, err := s.repo.GetByIDForUser(ctx, keyID, projectID, principal.User.ID) + if err != nil { + if errors.Is(err, userapikeyrepo.ErrNotFound) { + return domain.APIKey{}, ErrNotFound + } + return domain.APIKey{}, err + } + return item, nil +} + +func scopesForPermissions(permissions []humanauthdomain.PermissionKey) []string { + mapped := make([]string, 0) + for _, permission := range permissions { + switch permission { + case humanauthdomain.PermissionProjectRead: + mapped = append(mapped, string(agentplatform.ScopeActivityRead)) + case humanauthdomain.PermissionProjectUpdateRead: + mapped = append(mapped, string(agentplatform.ScopeProjectUpdatesRead)) + case humanauthdomain.PermissionProjectUpdateCreate, + humanauthdomain.PermissionProjectUpdateUpdate, + humanauthdomain.PermissionProjectUpdateDelete: + mapped = append(mapped, string(agentplatform.ScopeProjectUpdatesWrite)) + case humanauthdomain.PermissionRepoRead: + mapped = append(mapped, string(agentplatform.ScopeReposRead)) + case humanauthdomain.PermissionStatusRead: + mapped = append(mapped, string(agentplatform.ScopeStatusesList)) + case humanauthdomain.PermissionTicketRead: + mapped = append(mapped, string(agentplatform.ScopeTicketsList)) + case humanauthdomain.PermissionTicketCreate: + mapped = append(mapped, string(agentplatform.ScopeTicketsCreate)) + case humanauthdomain.PermissionTicketUpdate: + mapped = append(mapped, string(agentplatform.ScopeTicketsUpdate)) + case humanauthdomain.PermissionWorkflowRead, humanauthdomain.PermissionHarnessRead: + mapped = append(mapped, string(agentplatform.ScopeWorkflowsRead)) + } + } + return uniqueStrings(mapped) +} + +func uniqueStrings(items []string) []string { + result := make([]string, 0, len(items)) + for _, item := range items { + trimmed := strings.TrimSpace(item) + if trimmed == "" || slices.Contains(result, trimmed) { + continue + } + result = append(result, trimmed) + } + slices.Sort(result) + return result +} + +func intersectStrings(left, right []string) []string { + result := make([]string, 0, len(left)) + for _, item := range left { + if slices.Contains(right, item) { + result = append(result, item) + } + } + return uniqueStrings(result) +} + +func isSubset(left, right []string) bool { + for _, item := range left { + if !slices.Contains(right, item) { + return false + } + } + return true +} diff --git a/web/src/lib/api/contracts.ts b/web/src/lib/api/contracts.ts index f7fdb7732..5b76d1d9d 100644 --- a/web/src/lib/api/contracts.ts +++ b/web/src/lib/api/contracts.ts @@ -776,6 +776,22 @@ export type RetestGitHubOutboundCredentialResponse = DeepRequired< export type DeleteGitHubOutboundCredentialResponse = DeepRequired< ResponseFor<'/api/v1/projects/{projectId}/security-settings/github-outbound-credential', 'delete'> > +export type ProjectUserAPIKeyListResponse = DeepRequired< + ResponseFor<'/api/v1/projects/{projectId}/security-settings/api-keys', 'get'> +> +export type ProjectUserAPIKey = ItemOf +export type CreateProjectUserAPIKeyResponse = DeepRequired< + ResponseFor<'/api/v1/projects/{projectId}/security-settings/api-keys', 'post'> +> +export type RotateProjectUserAPIKeyResponse = DeepRequired< + ResponseFor<'/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/rotate', 'post'> +> +export type DisableProjectUserAPIKeyResponse = DeepRequired< + ResponseFor<'/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/disable', 'post'> +> +export type DeleteProjectUserAPIKeyResponse = DeepRequired< + ResponseFor<'/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}', 'delete'> +> // Org-level GitHub credential — managed under /orgs/:orgId/security/github-credential export type GitHubCredentialSlot = { diff --git a/web/src/lib/api/generated/openapi.d.ts b/web/src/lib/api/generated/openapi.d.ts index 04e23d80e..f8c4cc0af 100644 --- a/web/src/lib/api/generated/openapi.d.ts +++ b/web/src/lib/api/generated/openapi.d.ts @@ -1784,6 +1784,75 @@ export interface paths { patch?: never trace?: never } + '/api/v1/projects/{projectId}/security-settings/api-keys': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** List the current user's project-scoped API keys */ + get: operations['listProjectUserAPIKeys'] + put?: never + /** Create a new project-scoped user API key for the current human principal */ + post: operations['createProjectUserAPIKey'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + /** Delete a project-scoped user API key */ + delete: operations['deleteProjectUserAPIKey'] + options?: never + head?: never + patch?: never + trace?: never + } + '/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/disable': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** Disable a project-scoped user API key immediately */ + post: operations['disableProjectUserAPIKey'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/rotate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** Rotate a project-scoped user API key and return the new plaintext token once */ + post: operations['rotateProjectUserAPIKey'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/api/v1/projects/{projectId}/security-settings/github-outbound-credential': { parameters: { query?: never @@ -15869,6 +15938,19 @@ export interface operations { }[] runtime_secret_responses_redacted?: boolean } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } webhooks?: { connector_endpoint?: string } @@ -15938,7 +16020,7 @@ export interface operations { } } } - saveGitHubOutboundCredential: { + listProjectUserAPIKeys: { parameters: { query?: never header?: never @@ -15948,161 +16030,27 @@ export interface operations { } cookie?: never } - /** @description Save a platform-managed GitHub outbound credential request body. */ - requestBody: { - content: { - 'application/json': { - /** @description GitHub token value copied into platform-managed secret storage. */ - token?: string - } - } - } + requestBody?: never responses: { - /** @description Save a platform-managed GitHub outbound credential response. */ + /** @description List the current user's project-scoped API keys response. */ 200: { headers: { [name: string]: unknown } content: { 'application/json': { - security?: { - agent_tokens?: { - default_scopes?: string[] - environment_variable?: string - supported_project_scopes?: string[] - supported_scope_groups?: { - category?: string - scopes?: string[] - }[] - token_prefix?: string - transport?: string - } - approval_policies?: { - rules_count?: number - status?: string - summary?: string - } - auth?: { - active_mode?: string - bootstrap_state?: { - admin_emails?: string[] - status?: string - summary?: string - } - config_path?: string - configured_mode?: string - docs?: { - href?: string - summary?: string - title?: string - }[] - issuer_url?: string - last_validation?: { - authorization_endpoint?: string - checked_at?: string | null - issuer_url?: string - message?: string - redirect_url?: string - status?: string - token_endpoint?: string - warnings?: string[] - } - local_principal?: string - mode_summary?: string - next_steps?: string[] - oidc_draft?: { - allowed_email_domains?: string[] - bootstrap_admin_emails?: string[] - client_id?: string - client_secret_configured?: boolean - fixed_redirect_url?: string - issuer_url?: string - redirect_mode?: string - scopes?: string[] - } - public_exposure_risk?: string - recommended_mode?: string - session_policy?: { - session_idle_ttl?: string - session_ttl?: string - } - warnings?: string[] - } - deferred?: { - key?: string - summary?: string - title?: string - }[] - github?: { - effective?: { - configured?: boolean - probe?: { - checked_at?: string | null - configured?: boolean - last_error?: string - login?: string - permissions?: string[] - repo_access?: string - state?: string - valid?: boolean - } - scope?: string - source?: string - token_preview?: string - } - organization?: { - configured?: boolean - probe?: { - checked_at?: string | null - configured?: boolean - last_error?: string - login?: string - permissions?: string[] - repo_access?: string - state?: string - valid?: boolean - } - scope?: string - source?: string - token_preview?: string - } - project_override?: { - configured?: boolean - probe?: { - checked_at?: string | null - configured?: boolean - last_error?: string - login?: string - permissions?: string[] - repo_access?: string - state?: string - valid?: boolean - } - scope?: string - source?: string - token_preview?: string - } - } - project_id?: string - secret_hygiene?: { - legacy_machine_secret_env_vars?: number - legacy_machines_requiring_migration?: number - legacy_provider_inline_secret_bindings?: number - legacy_providers_requiring_migration?: number - machine_env_vars_redacted?: boolean - notification_channel_configs_redacted?: boolean - rollout_checklist?: { - key?: string - status?: string - summary?: string - title?: string - }[] - runtime_secret_responses_redacted?: boolean - } - webhooks?: { - connector_endpoint?: string - } - } + api_keys?: { + created_at?: string + disabled_at?: string | null + expires_at?: string | null + id?: string + last_used_at?: string | null + name?: string + revoked_at?: string | null + scopes?: string[] + status?: string + token_hint?: string + }[] } } } @@ -16118,8 +16066,8 @@ export interface operations { } } } - /** @description Not Found response. */ - 404: { + /** @description Unauthorized response. */ + 401: { headers: { [name: string]: unknown } @@ -16130,8 +16078,8 @@ export interface operations { } } } - /** @description Internal Server Error response. */ - 500: { + /** @description Not Found response. */ + 404: { headers: { [name: string]: unknown } @@ -16142,8 +16090,8 @@ export interface operations { } } } - /** @description Bad Gateway response. */ - 502: { + /** @description Internal Server Error response. */ + 500: { headers: { [name: string]: unknown } @@ -16168,7 +16116,7 @@ export interface operations { } } } - deleteGitHubOutboundCredential: { + createProjectUserAPIKey: { parameters: { query?: never header?: never @@ -16178,24 +16126,707 @@ export interface operations { } cookie?: never } - requestBody?: never + /** @description Create a new project-scoped user API key for the current human principal request body. */ + requestBody: { + content: { + 'application/json': { + expires_at?: string | null + name?: string + scopes?: string[] + } + } + } responses: { - /** @description Delete the project-level GitHub credential override response. */ - 200: { + /** @description Create a new project-scoped user API key for the current human principal response. */ + 201: { headers: { [name: string]: unknown } content: { 'application/json': { - security?: { - agent_tokens?: { - default_scopes?: string[] - environment_variable?: string - supported_project_scopes?: string[] - supported_scope_groups?: { - category?: string - scopes?: string[] - }[] + api_key?: { + created_at?: string + disabled_at?: string | null + expires_at?: string | null + id?: string + last_used_at?: string | null + name?: string + revoked_at?: string | null + scopes?: string[] + status?: string + token_hint?: string + } + plain_text_token?: string + } + } + } + /** @description Bad Request response. */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Unauthorized response. */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Forbidden response. */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Not Found response. */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Internal Server Error response. */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Service Unavailable response. */ + 503: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + } + } + deleteProjectUserAPIKey: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ID. */ + projectId: string + /** @description API key ID. */ + keyId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description User API key deleted. */ + 204: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Bad Request response. */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Unauthorized response. */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Forbidden response. */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Not Found response. */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Internal Server Error response. */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Service Unavailable response. */ + 503: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + default: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + disableProjectUserAPIKey: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ID. */ + projectId: string + /** @description API key ID. */ + keyId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Disable a project-scoped user API key immediately response. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + api_key?: { + created_at?: string + disabled_at?: string | null + expires_at?: string | null + id?: string + last_used_at?: string | null + name?: string + revoked_at?: string | null + scopes?: string[] + status?: string + token_hint?: string + } + } + } + } + /** @description Bad Request response. */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Unauthorized response. */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Forbidden response. */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Not Found response. */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Internal Server Error response. */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Service Unavailable response. */ + 503: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + } + } + rotateProjectUserAPIKey: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ID. */ + projectId: string + /** @description API key ID. */ + keyId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Rotate a project-scoped user API key and return the new plaintext token once response. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + api_key?: { + created_at?: string + disabled_at?: string | null + expires_at?: string | null + id?: string + last_used_at?: string | null + name?: string + revoked_at?: string | null + scopes?: string[] + status?: string + token_hint?: string + } + plain_text_token?: string + } + } + } + /** @description Bad Request response. */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Unauthorized response. */ + 401: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Forbidden response. */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Not Found response. */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Internal Server Error response. */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Service Unavailable response. */ + 503: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + } + } + saveGitHubOutboundCredential: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ID. */ + projectId: string + } + cookie?: never + } + /** @description Save a platform-managed GitHub outbound credential request body. */ + requestBody: { + content: { + 'application/json': { + /** @description GitHub token value copied into platform-managed secret storage. */ + token?: string + } + } + } + responses: { + /** @description Save a platform-managed GitHub outbound credential response. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + security?: { + agent_tokens?: { + default_scopes?: string[] + environment_variable?: string + supported_project_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + token_prefix?: string + transport?: string + } + approval_policies?: { + rules_count?: number + status?: string + summary?: string + } + auth?: { + active_mode?: string + bootstrap_state?: { + admin_emails?: string[] + status?: string + summary?: string + } + config_path?: string + configured_mode?: string + docs?: { + href?: string + summary?: string + title?: string + }[] + issuer_url?: string + last_validation?: { + authorization_endpoint?: string + checked_at?: string | null + issuer_url?: string + message?: string + redirect_url?: string + status?: string + token_endpoint?: string + warnings?: string[] + } + local_principal?: string + mode_summary?: string + next_steps?: string[] + oidc_draft?: { + allowed_email_domains?: string[] + bootstrap_admin_emails?: string[] + client_id?: string + client_secret_configured?: boolean + fixed_redirect_url?: string + issuer_url?: string + redirect_mode?: string + scopes?: string[] + } + public_exposure_risk?: string + recommended_mode?: string + session_policy?: { + session_idle_ttl?: string + session_ttl?: string + } + warnings?: string[] + } + deferred?: { + key?: string + summary?: string + title?: string + }[] + github?: { + effective?: { + configured?: boolean + probe?: { + checked_at?: string | null + configured?: boolean + last_error?: string + login?: string + permissions?: string[] + repo_access?: string + state?: string + valid?: boolean + } + scope?: string + source?: string + token_preview?: string + } + organization?: { + configured?: boolean + probe?: { + checked_at?: string | null + configured?: boolean + last_error?: string + login?: string + permissions?: string[] + repo_access?: string + state?: string + valid?: boolean + } + scope?: string + source?: string + token_preview?: string + } + project_override?: { + configured?: boolean + probe?: { + checked_at?: string | null + configured?: boolean + last_error?: string + login?: string + permissions?: string[] + repo_access?: string + state?: string + valid?: boolean + } + scope?: string + source?: string + token_preview?: string + } + } + project_id?: string + secret_hygiene?: { + legacy_machine_secret_env_vars?: number + legacy_machines_requiring_migration?: number + legacy_provider_inline_secret_bindings?: number + legacy_providers_requiring_migration?: number + machine_env_vars_redacted?: boolean + notification_channel_configs_redacted?: boolean + rollout_checklist?: { + key?: string + status?: string + summary?: string + title?: string + }[] + runtime_secret_responses_redacted?: boolean + } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } + webhooks?: { + connector_endpoint?: string + } + } + } + } + } + /** @description Bad Request response. */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Not Found response. */ + 404: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Internal Server Error response. */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Bad Gateway response. */ + 502: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + /** @description Service Unavailable response. */ + 503: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + code?: string + message?: string + } + } + } + } + } + deleteGitHubOutboundCredential: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ID. */ + projectId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description Delete the project-level GitHub credential override response. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': { + security?: { + agent_tokens?: { + default_scopes?: string[] + environment_variable?: string + supported_project_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] token_prefix?: string transport?: string } @@ -16321,6 +16952,19 @@ export interface operations { }[] runtime_secret_responses_redacted?: boolean } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } webhooks?: { connector_endpoint?: string } @@ -16543,6 +17187,19 @@ export interface operations { }[] runtime_secret_responses_redacted?: boolean } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } webhooks?: { connector_endpoint?: string } @@ -16765,6 +17422,19 @@ export interface operations { }[] runtime_secret_responses_redacted?: boolean } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } webhooks?: { connector_endpoint?: string } @@ -17015,6 +17685,19 @@ export interface operations { }[] runtime_secret_responses_redacted?: boolean } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } webhooks?: { connector_endpoint?: string } @@ -17390,6 +18073,19 @@ export interface operations { }[] runtime_secret_responses_redacted?: boolean } + user_api_keys?: { + allowed_scope_groups?: { + category?: string + scopes?: string[] + }[] + allowed_scopes?: string[] + supported_scope_groups?: { + category?: string + scopes?: string[] + }[] + supported_scopes?: string[] + token_prefix?: string + } webhooks?: { connector_endpoint?: string } diff --git a/web/src/lib/api/openase.ts b/web/src/lib/api/openase.ts index 3c78d6e89..c6415b178 100644 --- a/web/src/lib/api/openase.ts +++ b/web/src/lib/api/openase.ts @@ -20,9 +20,12 @@ import type { ArchivedTicketPayload, BuiltinRolePayload, BuiltinRoleDetailResponse, + CreateProjectUserAPIKeyResponse, CreateScopedSecretBindingResponse, + DeleteProjectUserAPIKeyResponse, DeleteScopedSecretBindingResponse, DeleteGitHubOutboundCredentialResponse, + DisableProjectUserAPIKeyResponse, OrgGitHubCredentialResponse, GitHubRepositoryCreateResponse, GitHubRepositoryListResponse, @@ -52,6 +55,7 @@ import type { OIDCEnableResponse, ProjectRepoPayload, ProjectArchiveResponse, + ProjectUserAPIKeyListResponse, ProjectUpdateCommentCreateResponse, ProjectUpdateCommentDeleteResponse, ProjectUpdateCommentResponse, @@ -75,6 +79,7 @@ import type { ScopedSecretResponse, ScopedSecretsResponse, RetestGitHubOutboundCredentialResponse, + RotateProjectUserAPIKeyResponse, SaveGitHubOutboundCredentialResponse, SkillListPayload, SkillCreateResponse, @@ -427,6 +432,40 @@ export function listScopedSecrets(projectId: string) { return api.get(`/api/v1/projects/${projectId}/security-settings/secrets`) } +export function listProjectUserAPIKeys(projectId: string) { + return api.get( + `/api/v1/projects/${projectId}/security-settings/api-keys`, + ) +} + +export function createProjectUserAPIKey( + projectId: string, + body: { name: string; scopes: string[]; expires_at?: string | null }, +) { + return api.post( + `/api/v1/projects/${projectId}/security-settings/api-keys`, + { body }, + ) +} + +export function rotateProjectUserAPIKey(projectId: string, keyId: string) { + return api.post( + `/api/v1/projects/${projectId}/security-settings/api-keys/${keyId}/rotate`, + ) +} + +export function disableProjectUserAPIKey(projectId: string, keyId: string) { + return api.post( + `/api/v1/projects/${projectId}/security-settings/api-keys/${keyId}/disable`, + ) +} + +export function deleteProjectUserAPIKey(projectId: string, keyId: string) { + return api.delete( + `/api/v1/projects/${projectId}/security-settings/api-keys/${keyId}`, + ) +} + export function listScopedSecretBindings(projectId: string) { return api.get( `/api/v1/projects/${projectId}/security-settings/secret-bindings`, diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte new file mode 100644 index 000000000..d1527586e --- /dev/null +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte @@ -0,0 +1,341 @@ + + +
+
+
+

User API keys

+

+ Create project-scoped API keys for external automation. Plaintext tokens are only shown + once. +

+
+ + + {#snippet child({ props })} + + {/snippet} + + + + Create project API key + + Choose a name, an optional expiry, and the scopes this key should carry. + + + +
+ + (name = (event.currentTarget as HTMLInputElement).value)} + /> +
+
+ + + (expiresAtLocal = (event.currentTarget as HTMLInputElement).value)} + /> +
+
+ + (selectedScopes = scopes)} + disabled={mutationKey === 'create'} + /> + {#if scopeGroups.length === 0} +

+ No mintable scopes are available for your current project permissions. +

+ {/if} +
+
+ + + {#snippet child({ props })} + + {/snippet} + + + +
+
+
+ + {#if loading} +
Loading API keys...
+ {:else if error} +
{error}
+ {:else if items.length === 0} +
+ No project API keys yet. +
+ {:else} +
+ {#each items as item (item.id)} +
+
+
+
+
{item.name}
+ {item.status} +
+
{item.token_hint}
+
+ Created {formatTime(item.created_at)} · Last used {formatTime(item.last_used_at)} · Expires + {formatTime(item.expires_at)} +
+
+
+ + + +
+
+
+ {#each item.scopes as scope (scope)} + {scope} + {/each} +
+
+ {/each} +
+ {/if} +
+ + + + + Copy this API key now + + {currentTokenName} was created successfully. This plaintext token is only shown once. + + + +
{currentToken}
+

+ Store it in your secret manager before closing this dialog. OpenASE only keeps a hashed + preview. +

+
+ + + + {#snippet child({ props })} + + {/snippet} + + +
+
diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts b/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts new file mode 100644 index 000000000..b169a0943 --- /dev/null +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts @@ -0,0 +1,93 @@ +import { cleanup, fireEvent, render, waitFor } from '@testing-library/svelte' +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +import { appStore } from '$lib/stores/app.svelte' +import { configuredSecurity, currentOrg, currentProject } from './security-settings.test-helpers' +import SecuritySettingsUserAPIKeys from './security-settings-user-api-keys.svelte' + +const { + createProjectUserAPIKey, + deleteProjectUserAPIKey, + disableProjectUserAPIKey, + listProjectUserAPIKeys, + rotateProjectUserAPIKey, +} = vi.hoisted(() => ({ + createProjectUserAPIKey: vi.fn(), + deleteProjectUserAPIKey: vi.fn(), + disableProjectUserAPIKey: vi.fn(), + listProjectUserAPIKeys: vi.fn(), + rotateProjectUserAPIKey: vi.fn(), +})) + +vi.mock('$lib/api/openase', () => ({ + createProjectUserAPIKey, + deleteProjectUserAPIKey, + disableProjectUserAPIKey, + listProjectUserAPIKeys, + rotateProjectUserAPIKey, +})) + +describe('SecuritySettingsUserAPIKeys', () => { + beforeAll(() => { + HTMLElement.prototype.scrollIntoView ??= vi.fn() + Object.assign(globalThis.navigator, { + clipboard: { + writeText: vi.fn().mockResolvedValue(undefined), + }, + }) + }) + + afterEach(() => { + cleanup() + appStore.currentOrg = null + appStore.currentProject = null + vi.clearAllMocks() + }) + + it('shows plaintext API key once after creating a project user API key', async () => { + appStore.currentOrg = currentOrg() + appStore.currentProject = currentProject() + listProjectUserAPIKeys.mockResolvedValue({ api_keys: [] }) + createProjectUserAPIKey.mockResolvedValue({ + api_key: { + id: 'key-1', + name: 'Buildkite', + token_hint: 'ase_pat_abc...1234', + scopes: ['tickets.list'], + status: 'active', + expires_at: null, + last_used_at: null, + created_at: '2026-04-16T12:00:00Z', + disabled_at: null, + revoked_at: null, + }, + plain_text_token: 'ase_pat_plain_text_token', + }) + + const { findByLabelText, findAllByRole, findByText, queryByText } = render( + SecuritySettingsUserAPIKeys, + { security: configuredSecurity() }, + ) + + await fireEvent.click((await findAllByRole('button', { name: 'Create key' }))[0]) + await fireEvent.input(await findByLabelText('Name'), { target: { value: 'Buildkite' } }) + await fireEvent.click(await findByText('tickets')) + await fireEvent.click(await findByText('list')) + await fireEvent.click((await findAllByRole('button', { name: 'Create key' }))[1]) + + await waitFor(() => { + expect(createProjectUserAPIKey).toHaveBeenCalledWith(appStore.currentProject?.id, { + name: 'Buildkite', + scopes: ['tickets.list'], + expires_at: null, + }) + }) + + expect(await findByText('Copy this API key now')).toBeTruthy() + expect(await findByText('ase_pat_plain_text_token')).toBeTruthy() + await fireEvent.click(await findByText('Done')) + await waitFor(() => { + expect(queryByText('ase_pat_plain_text_token')).toBeNull() + }) + }) +}) diff --git a/web/src/lib/features/settings/components/security-settings.svelte b/web/src/lib/features/settings/components/security-settings.svelte index 2aac11fc9..ce78b309f 100644 --- a/web/src/lib/features/settings/components/security-settings.svelte +++ b/web/src/lib/features/settings/components/security-settings.svelte @@ -19,6 +19,7 @@ import ProjectScopedSecretsPanel from './project-scoped-secrets-panel.svelte' import SecurityPlatformDetails from './security-settings-platform-details.svelte' import SecuritySettingsSecretBindingsSection from './security-settings-secret-bindings-section.svelte' + import SecuritySettingsUserAPIKeys from './security-settings-user-api-keys.svelte' import { normalizeSecuritySettings } from '../security-settings' type Security = SecuritySettingsResponse['security'] @@ -168,6 +169,10 @@ + + + + diff --git a/web/src/lib/features/settings/components/security-settings.test-helpers.ts b/web/src/lib/features/settings/components/security-settings.test-helpers.ts index 569f5f42e..def581950 100644 --- a/web/src/lib/features/settings/components/security-settings.test-helpers.ts +++ b/web/src/lib/features/settings/components/security-settings.test-helpers.ts @@ -9,7 +9,12 @@ export function currentProject() { /* i18n-exempt */ description: '', /* i18n-exempt */ status: 'active', default_agent_provider_id: null, - project_ai_platform_access_allowed: ['project_updates.read', 'project_updates.write', 'projects.update', 'projects.add_repo'], + project_ai_platform_access_allowed: [ + 'project_updates.read', + 'project_updates.write', + 'projects.update', + 'projects.add_repo', + ], accessible_machine_ids: [], max_concurrent_agents: 4, } @@ -108,11 +113,48 @@ export function configuredSecurity() { environment_variable: 'OPENASE_AGENT_TOKEN', token_prefix: 'ase_agent_', default_scopes: ['tickets.create', 'tickets.list'], - supported_project_scopes: ['project_updates.read', 'project_updates.write', 'projects.update', 'projects.add_repo'], + supported_project_scopes: [ + 'project_updates.read', + 'project_updates.write', + 'projects.update', + 'projects.add_repo', + ], supported_scope_groups: [ { category: 'projects', - scopes: ['project_updates.read', 'project_updates.write', 'projects.update', 'projects.add_repo'], + scopes: [ + 'project_updates.read', + 'project_updates.write', + 'projects.update', + 'projects.add_repo', + ], + }, + ], + }, + user_api_keys: { + token_prefix: 'ase_pat_', + supported_scopes: [ + 'activity.read', + 'project_updates.read', + 'project_updates.write', + 'repos.read', + 'statuses.list', + 'tickets.create', + 'tickets.list', + 'tickets.update', + 'workflows.read', + ], + supported_scope_groups: [ + { + category: 'tickets', + scopes: ['tickets.create', 'tickets.list', 'tickets.update'], + }, + ], + allowed_scopes: ['tickets.create', 'tickets.list', 'tickets.update'], + allowed_scope_groups: [ + { + category: 'tickets', + scopes: ['tickets.create', 'tickets.list', 'tickets.update'], }, ], }, diff --git a/web/src/lib/features/settings/components/security-settings.test.ts b/web/src/lib/features/settings/components/security-settings.test.ts index 2abe4cd5b..a5c3899cb 100644 --- a/web/src/lib/features/settings/components/security-settings.test.ts +++ b/web/src/lib/features/settings/components/security-settings.test.ts @@ -20,37 +20,47 @@ import { const { createProjectScopedSecret, + createProjectUserAPIKey, createScopedSecretBinding, deleteGitHubOutboundCredential, deleteProjectScopedSecret, + deleteProjectUserAPIKey, deleteScopedSecretBinding, disableProjectScopedSecret, + disableProjectUserAPIKey, getSecuritySettings, importGitHubOutboundCredentialFromGHCLI, listProjectScopedSecrets, + listProjectUserAPIKeys, listScopedSecretBindings, listScopedSecrets, listTickets, listWorkflows, retestGitHubOutboundCredential, + rotateProjectUserAPIKey, rotateProjectScopedSecret, saveGitHubOutboundCredential, updateProject, } = vi.hoisted(() => ({ createProjectScopedSecret: vi.fn(), + createProjectUserAPIKey: vi.fn(), createScopedSecretBinding: vi.fn(), deleteGitHubOutboundCredential: vi.fn(), deleteProjectScopedSecret: vi.fn(), + deleteProjectUserAPIKey: vi.fn(), deleteScopedSecretBinding: vi.fn(), disableProjectScopedSecret: vi.fn(), + disableProjectUserAPIKey: vi.fn(), getSecuritySettings: vi.fn(), importGitHubOutboundCredentialFromGHCLI: vi.fn(), listProjectScopedSecrets: vi.fn(), + listProjectUserAPIKeys: vi.fn(), listScopedSecretBindings: vi.fn(), listScopedSecrets: vi.fn(), listTickets: vi.fn(), listWorkflows: vi.fn(), retestGitHubOutboundCredential: vi.fn(), + rotateProjectUserAPIKey: vi.fn(), rotateProjectScopedSecret: vi.fn(), saveGitHubOutboundCredential: vi.fn(), updateProject: vi.fn(), @@ -58,19 +68,24 @@ const { vi.mock('$lib/api/openase', () => ({ createProjectScopedSecret, + createProjectUserAPIKey, createScopedSecretBinding, deleteGitHubOutboundCredential, deleteProjectScopedSecret, + deleteProjectUserAPIKey, deleteScopedSecretBinding, disableProjectScopedSecret, + disableProjectUserAPIKey, getSecuritySettings, importGitHubOutboundCredentialFromGHCLI, listProjectScopedSecrets, + listProjectUserAPIKeys, listScopedSecretBindings, listScopedSecrets, listTickets, listWorkflows, retestGitHubOutboundCredential, + rotateProjectUserAPIKey, rotateProjectScopedSecret, saveGitHubOutboundCredential, updateProject, @@ -101,6 +116,10 @@ describe('Security settings', () => { listProjectScopedSecrets.mockResolvedValue({ secrets }) } + function mockProjectUserAPIKeys(items: Array> = []) { + listProjectUserAPIKeys.mockResolvedValue({ api_keys: items }) + } + it('renders project-owned security controls', async () => { appStore.currentOrg = currentOrg() appStore.currentProject = currentProject() @@ -130,6 +149,7 @@ describe('Security settings', () => { }, }, ]) + mockProjectUserAPIKeys() const { findByText } = render(SecuritySettings) @@ -137,6 +157,7 @@ describe('Security settings', () => { expect(await findByText('Inherited organization defaults')).toBeTruthy() expect(await findByText('GitHub outbound credentials')).toBeTruthy() expect(await findByText('Runtime secret bindings')).toBeTruthy() + expect(await findByText('User API keys')).toBeTruthy() expect(await findByText('Project AI platform access')).toBeTruthy() expect(await findByText('Fullstack Developer Workflow')).toBeTruthy() expect(await findByText('OPENASE_AGENT_TOKEN')).toBeTruthy() @@ -148,6 +169,7 @@ describe('Security settings', () => { getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() saveGitHubOutboundCredential.mockResolvedValue({ security: configuredSecurity() }) const { findByPlaceholderText, findAllByRole } = render(SecuritySettings) @@ -171,6 +193,7 @@ describe('Security settings', () => { getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() updateProject.mockResolvedValue({ project: { ...currentProject(), @@ -198,6 +221,7 @@ describe('Security settings', () => { getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() updateProject.mockResolvedValue({ project: { ...currentProject(), @@ -226,6 +250,7 @@ describe('Security settings', () => { getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() const { findByText, queryByText } = render(SecuritySettings) @@ -240,6 +265,7 @@ describe('Security settings', () => { getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() const importedSecurity = configuredSecurityWithProjectGitHubOverride() importGitHubOutboundCredentialFromGHCLI.mockResolvedValue({ security: importedSecurity }) retestGitHubOutboundCredential.mockResolvedValue({ security: importedSecurity }) @@ -276,6 +302,7 @@ describe('Security settings', () => { }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() const { findByText, queryByText } = render(SecuritySettings) @@ -291,6 +318,7 @@ describe('Security settings', () => { mockSecretBindingCatalog() listScopedSecrets.mockResolvedValue({ secrets: [scopedSecrets()[0]] }) mockProjectScopedSecrets() + mockProjectUserAPIKeys() listScopedSecretBindings.mockResolvedValue({ bindings: [] }) createScopedSecretBinding.mockResolvedValue({ binding: scopedSecretBindings()[0], @@ -331,6 +359,7 @@ describe('Security settings', () => { getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) mockSecretBindingCatalog() mockProjectScopedSecrets() + mockProjectUserAPIKeys() deleteScopedSecretBinding.mockResolvedValue({}) const { findByTitle } = render(SecuritySettings) @@ -344,4 +373,51 @@ describe('Security settings', () => { ) }) }) + + it('shows plaintext API key once after creating a project user API key', async () => { + appStore.currentOrg = currentOrg() + appStore.currentProject = currentProject() + getSecuritySettings.mockResolvedValue({ security: configuredSecurity() }) + mockSecretBindingCatalog() + mockProjectScopedSecrets() + mockProjectUserAPIKeys() + createProjectUserAPIKey.mockResolvedValue({ + api_key: { + id: 'key-1', + name: 'Buildkite', + token_hint: 'ase_pat_abc...1234', + scopes: ['tickets.list'], + status: 'active', + expires_at: null, + last_used_at: null, + created_at: '2026-04-16T12:00:00Z', + disabled_at: null, + revoked_at: null, + }, + plain_text_token: 'ase_pat_plain_text_token', + }) + + const { findByLabelText, findByRole, findByText, queryByText } = render(SecuritySettings) + + await fireEvent.click(await findByRole('button', { name: 'Create key' })) + await fireEvent.input(await findByLabelText('Name'), { target: { value: 'Buildkite' } }) + await fireEvent.click(await findByText('tickets')) + await fireEvent.click(await findByText('list')) + await fireEvent.click(await findByRole('button', { name: 'Create key' })) + + await waitFor(() => { + expect(createProjectUserAPIKey).toHaveBeenCalledWith(appStore.currentProject?.id, { + name: 'Buildkite', + scopes: ['tickets.list'], + expires_at: null, + }) + }) + + expect(await findByText('Copy this API key now')).toBeTruthy() + expect(await findByText('ase_pat_plain_text_token')).toBeTruthy() + await fireEvent.click(await findByRole('button', { name: 'Done' })) + await waitFor(() => { + expect(queryByText('ase_pat_plain_text_token')).toBeNull() + }) + }) }) diff --git a/web/src/lib/features/settings/security-settings.ts b/web/src/lib/features/settings/security-settings.ts index 8b3c9ab4f..ef10ab101 100644 --- a/web/src/lib/features/settings/security-settings.ts +++ b/web/src/lib/features/settings/security-settings.ts @@ -8,6 +8,21 @@ export function normalizeSecuritySettings( ): SecuritySettingsResponse['security'] { return { ...security, + user_api_keys: { + ...security.user_api_keys, + supported_scopes: Array.isArray(security.user_api_keys.supported_scopes) + ? security.user_api_keys.supported_scopes + : [], + supported_scope_groups: Array.isArray(security.user_api_keys.supported_scope_groups) + ? security.user_api_keys.supported_scope_groups + : [], + allowed_scopes: Array.isArray(security.user_api_keys.allowed_scopes) + ? security.user_api_keys.allowed_scopes + : [], + allowed_scope_groups: Array.isArray(security.user_api_keys.allowed_scope_groups) + ? security.user_api_keys.allowed_scope_groups + : [], + }, github: { ...security.github, effective: normalizeGitHubSlot(security.github.effective), From dc66a60f2421383cce205bf1604dbab9945eb776 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 16:37:56 +0100 Subject: [PATCH 02/14] fix(ci): unblock lint and frontend formatting --- internal/domain/agentplatform/contracts.go | 3 ++- web/src/lib/features/agents/components/agents-page.test.ts | 7 ++++--- web/src/lib/testing/e2e/mock-api/security.ts | 7 ++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/domain/agentplatform/contracts.go b/internal/domain/agentplatform/contracts.go index 6a12b8ac8..0dd222b48 100644 --- a/internal/domain/agentplatform/contracts.go +++ b/internal/domain/agentplatform/contracts.go @@ -101,7 +101,8 @@ type PrincipalKind string const ( PrincipalKindTicketAgent PrincipalKind = "ticket_agent" PrincipalKindProjectConversation PrincipalKind = "project_conversation" - PrincipalKindUserAPIKey PrincipalKind = "user_api_key" + // #nosec G101 -- principal kind label, not a credential. + PrincipalKindUserAPIKey PrincipalKind = "user_api_key" ) type ScopeSet []Scope diff --git a/web/src/lib/features/agents/components/agents-page.test.ts b/web/src/lib/features/agents/components/agents-page.test.ts index 89baca92f..ae6885660 100644 --- a/web/src/lib/features/agents/components/agents-page.test.ts +++ b/web/src/lib/features/agents/components/agents-page.test.ts @@ -21,9 +21,10 @@ vi.mock('$lib/api/sse', () => ({ vi.mock('$lib/features/project-events', () => ({ createProjectReconnectRecoveryTask: vi.fn( - (recover: (recovery: { sequence: number }) => Promise | void) => (recovery: { sequence: number }) => { - void recover(recovery) - }, + (recover: (recovery: { sequence: number }) => Promise | void) => + (recovery: { sequence: number }) => { + void recover(recovery) + }, ), isProjectDashboardRefreshEvent: vi.fn(() => false), readProjectDashboardRefreshSections: vi.fn(() => []), diff --git a/web/src/lib/testing/e2e/mock-api/security.ts b/web/src/lib/testing/e2e/mock-api/security.ts index 6e46f2994..b11f31086 100644 --- a/web/src/lib/testing/e2e/mock-api/security.ts +++ b/web/src/lib/testing/e2e/mock-api/security.ts @@ -99,7 +99,12 @@ export function createDefaultSecuritySettings(projectId: string): MockSecuritySe environment_variable: 'OPENASE_AGENT_TOKEN', token_prefix: 'ase_agent_', default_scopes: ['tickets.create', 'tickets.list'], - supported_project_scopes: ['project_updates.read', 'project_updates.write', 'projects.update', 'projects.add_repo'], + supported_project_scopes: [ + 'project_updates.read', + 'project_updates.write', + 'projects.update', + 'projects.add_repo', + ], }, github: { effective: clone(organization), From 9ecbc3c3e431582751b1158a4a2e745a6f3d8ab6 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 16:41:23 +0100 Subject: [PATCH 03/14] fix(user-api-keys): satisfy architecture and lint rules --- internal/service/userapikey/service.go | 75 ++++++++++++++----- .../security-settings-user-api-keys.svelte | 5 -- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/internal/service/userapikey/service.go b/internal/service/userapikey/service.go index 5d826d58b..ba1d116fa 100644 --- a/internal/service/userapikey/service.go +++ b/internal/service/userapikey/service.go @@ -3,6 +3,9 @@ package userapikey import ( "context" "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "errors" "fmt" "io" @@ -10,7 +13,6 @@ import ( "strings" "time" - "github.com/BetterAndBetterII/openase/internal/agentplatform" agentplatformdomain "github.com/BetterAndBetterII/openase/internal/domain/agentplatform" humanauthdomain "github.com/BetterAndBetterII/openase/internal/domain/humanauth" domain "github.com/BetterAndBetterII/openase/internal/domain/userapikey" @@ -52,7 +54,7 @@ func (s *Service) List(ctx context.Context, projectID uuid.UUID, principal human } func (s *Service) AllowedScopes(ctx context.Context, projectID uuid.UUID, principal *humanauthdomain.AuthenticatedPrincipal) ([]string, []agentplatformdomain.ScopeGroup, error) { - requestedSupported := agentplatform.SupportedScopesForPrincipalKind(agentplatform.PrincipalKindUserAPIKey) + requestedSupported := agentplatformdomain.SupportedScopesForPrincipalKind(agentplatformdomain.PrincipalKindUserAPIKey) if principal == nil { return requestedSupported, domain.SupportedScopeGroups(requestedSupported), nil } @@ -89,7 +91,7 @@ func (s *Service) Create(ctx context.Context, principal humanauthdomain.Authenti if err != nil { return domain.CreateResult{}, err } - requested, err := agentplatform.ParseExplicitScopesForPrincipalKind(agentplatform.PrincipalKindUserAPIKey, input.Scopes) + requested, err := parseSupportedUserAPIKeyScopes(input.Scopes) if err != nil { return domain.CreateResult{}, fmt.Errorf("%w: %v", ErrInvalidInput, err) } @@ -102,7 +104,7 @@ func (s *Service) Create(ctx context.Context, principal humanauthdomain.Authenti if input.ExpiresAt != nil && !input.ExpiresAt.After(s.now().UTC()) { return domain.CreateResult{}, fmt.Errorf("%w: expires_at must be in the future", ErrInvalidInput) } - plainTextToken, tokenHash, err := agentplatform.GenerateOpaqueToken(agentplatform.UserAPIKeyTokenPrefix(), s.rng) + plainTextToken, tokenHash, err := generateOpaqueToken(agentplatformdomain.UserAPIKeyTokenPrefix, s.rng) if err != nil { return domain.CreateResult{}, err } @@ -110,8 +112,8 @@ func (s *Service) Create(ctx context.Context, principal humanauthdomain.Authenti UserID: input.UserID, ProjectID: input.ProjectID, Name: input.Name, - TokenPrefix: agentplatform.UserAPIKeyTokenPrefix(), - TokenHint: agentplatform.TokenPreview(plainTextToken), + TokenPrefix: agentplatformdomain.UserAPIKeyTokenPrefix, + TokenHint: tokenPreview(plainTextToken), TokenHash: tokenHash, Scopes: requested, ExpiresAt: input.ExpiresAt, @@ -151,14 +153,14 @@ func (s *Service) Rotate(ctx context.Context, projectID, keyID uuid.UUID, princi if !isSubset(item.Scopes, allowedScopes) { return domain.CreateResult{}, ErrScopeForbidden } - plainTextToken, tokenHash, err := agentplatform.GenerateOpaqueToken(agentplatform.UserAPIKeyTokenPrefix(), s.rng) + plainTextToken, tokenHash, err := generateOpaqueToken(agentplatformdomain.UserAPIKeyTokenPrefix, s.rng) if err != nil { return domain.CreateResult{}, err } rotated, err := s.repo.Rotate(ctx, keyID, userapikeyrepo.RotateRecord{ Name: item.Name, - TokenPrefix: agentplatform.UserAPIKeyTokenPrefix(), - TokenHint: agentplatform.TokenPreview(plainTextToken), + TokenPrefix: agentplatformdomain.UserAPIKeyTokenPrefix, + TokenHint: tokenPreview(plainTextToken), TokenHash: tokenHash, Scopes: item.Scopes, ExpiresAt: item.ExpiresAt, @@ -209,25 +211,25 @@ func scopesForPermissions(permissions []humanauthdomain.PermissionKey) []string for _, permission := range permissions { switch permission { case humanauthdomain.PermissionProjectRead: - mapped = append(mapped, string(agentplatform.ScopeActivityRead)) + mapped = append(mapped, string(agentplatformdomain.ScopeActivityRead)) case humanauthdomain.PermissionProjectUpdateRead: - mapped = append(mapped, string(agentplatform.ScopeProjectUpdatesRead)) + mapped = append(mapped, string(agentplatformdomain.ScopeProjectUpdatesRead)) case humanauthdomain.PermissionProjectUpdateCreate, humanauthdomain.PermissionProjectUpdateUpdate, humanauthdomain.PermissionProjectUpdateDelete: - mapped = append(mapped, string(agentplatform.ScopeProjectUpdatesWrite)) + mapped = append(mapped, string(agentplatformdomain.ScopeProjectUpdatesWrite)) case humanauthdomain.PermissionRepoRead: - mapped = append(mapped, string(agentplatform.ScopeReposRead)) + mapped = append(mapped, string(agentplatformdomain.ScopeReposRead)) case humanauthdomain.PermissionStatusRead: - mapped = append(mapped, string(agentplatform.ScopeStatusesList)) + mapped = append(mapped, string(agentplatformdomain.ScopeStatusesList)) case humanauthdomain.PermissionTicketRead: - mapped = append(mapped, string(agentplatform.ScopeTicketsList)) + mapped = append(mapped, string(agentplatformdomain.ScopeTicketsList)) case humanauthdomain.PermissionTicketCreate: - mapped = append(mapped, string(agentplatform.ScopeTicketsCreate)) + mapped = append(mapped, string(agentplatformdomain.ScopeTicketsCreate)) case humanauthdomain.PermissionTicketUpdate: - mapped = append(mapped, string(agentplatform.ScopeTicketsUpdate)) + mapped = append(mapped, string(agentplatformdomain.ScopeTicketsUpdate)) case humanauthdomain.PermissionWorkflowRead, humanauthdomain.PermissionHarnessRead: - mapped = append(mapped, string(agentplatform.ScopeWorkflowsRead)) + mapped = append(mapped, string(agentplatformdomain.ScopeWorkflowsRead)) } } return uniqueStrings(mapped) @@ -264,3 +266,40 @@ func isSubset(left, right []string) bool { } return true } + +func parseSupportedUserAPIKeyScopes(raw []string) ([]string, error) { + supported := agentplatformdomain.SupportedScopesForPrincipalKind(agentplatformdomain.PrincipalKindUserAPIKey) + result := make([]string, 0, len(raw)) + for _, item := range raw { + scope := strings.TrimSpace(item) + if scope == "" { + return nil, fmt.Errorf("scope must not be empty") + } + if !slices.Contains(supported, scope) { + return nil, fmt.Errorf("unsupported scope %q", scope) + } + if !slices.Contains(result, scope) { + result = append(result, scope) + } + } + slices.Sort(result) + return result, nil +} + +func generateOpaqueToken(prefix string, rng io.Reader) (string, string, error) { + bytes := make([]byte, 24) + if _, err := io.ReadFull(rng, bytes); err != nil { + return "", "", fmt.Errorf("generate user api key bytes: %w", err) + } + token := prefix + base64.RawURLEncoding.EncodeToString(bytes) + sum := sha256.Sum256([]byte(token)) + return token, hex.EncodeToString(sum[:]), nil +} + +func tokenPreview(token string) string { + trimmed := strings.TrimSpace(token) + if len(trimmed) <= 18 { + return trimmed + } + return trimmed[:12] + "..." + trimmed[len(trimmed)-4:] +} diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte index d1527586e..ae02a7071 100644 --- a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte @@ -41,7 +41,6 @@ let mutationKey = $state('') const scopeGroups = $derived(security.user_api_keys.allowed_scope_groups ?? []) - const availableScopes = $derived(security.user_api_keys.allowed_scopes ?? []) $effect(() => { const projectId = appStore.currentProject?.id @@ -68,10 +67,6 @@ } }) - $effect(() => { - selectedScopes = [] - }) - function formatError(caughtError: unknown): string { return caughtError instanceof ApiError ? caughtError.detail : 'Request failed' } From a4ba5bffeb1ccbafb061f0862dfc75a749bde3d3 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 16:46:32 +0100 Subject: [PATCH 04/14] fix(user-api-keys): satisfy frontend i18n and architecture checks --- .../security-settings-user-api-keys.svelte | 85 ++++++++++++------- web/src/lib/i18n/locales/en.json | 28 ++++++ web/src/lib/i18n/locales/zh.json | 28 ++++++ 3 files changed, 109 insertions(+), 32 deletions(-) diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte index ae02a7071..25fa2008e 100644 --- a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte @@ -20,6 +20,7 @@ import * as Dialog from '$ui/dialog' import { Input } from '$ui/input' import { Label } from '$ui/label' + import { i18nStore } from '$lib/i18n/store.svelte' type Security = SecuritySettingsResponse['security'] type PlainTextResult = @@ -68,7 +69,9 @@ }) function formatError(caughtError: unknown): string { - return caughtError instanceof ApiError ? caughtError.detail : 'Request failed' + return caughtError instanceof ApiError + ? caughtError.detail + : i18nStore.t('settings.security.userApiKeys.errors.requestFailed') } function resetDraft() { @@ -103,7 +106,7 @@ }) items = [payload.api_key, ...items] showPlainTextToken(payload) - toastStore.success('API key created') + toastStore.success(i18nStore.t('settings.security.userApiKeys.notifications.created')) createOpen = false resetDraft() } catch (caughtError) { @@ -127,7 +130,7 @@ const payload = await rotateProjectUserAPIKey(projectId, item.id) items = items.map((candidate) => (candidate.id === item.id ? payload.api_key : candidate)) showPlainTextToken(payload) - toastStore.success('API key rotated') + toastStore.success(i18nStore.t('settings.security.userApiKeys.notifications.rotated')) } catch (caughtError) { toastStore.error(formatError(caughtError)) } finally { @@ -145,7 +148,7 @@ item.id, ) items = items.map((candidate) => (candidate.id === item.id ? payload.api_key : candidate)) - toastStore.success('API key disabled') + toastStore.success(i18nStore.t('settings.security.userApiKeys.notifications.disabled')) } catch (caughtError) { toastStore.error(formatError(caughtError)) } finally { @@ -160,7 +163,7 @@ try { await deleteProjectUserAPIKey(projectId, item.id) items = items.filter((candidate) => candidate.id !== item.id) - toastStore.success('API key deleted') + toastStore.success(i18nStore.t('settings.security.userApiKeys.notifications.deleted')) } catch (caughtError) { toastStore.error(formatError(caughtError)) } finally { @@ -171,9 +174,9 @@ async function copyToken() { try { await navigator.clipboard.writeText(currentToken) - toastStore.success('Token copied') + toastStore.success(i18nStore.t('settings.security.userApiKeys.notifications.copied')) } catch { - toastStore.error('Copy failed') + toastStore.error(i18nStore.t('settings.security.userApiKeys.errors.copyFailed')) } } @@ -181,28 +184,31 @@
-

User API keys

+

{i18nStore.t('settings.security.userApiKeys.heading')}

- Create project-scoped API keys for external automation. Plaintext tokens are only shown - once. + {i18nStore.t('settings.security.userApiKeys.description')}

{#snippet child({ props })} - + {/snippet} - Create project API key + {i18nStore.t('settings.security.userApiKeys.dialog.title')} - Choose a name, an optional expiry, and the scopes this key should carry. + {i18nStore.t('settings.security.userApiKeys.dialog.description')}
- +
- +
- + {#if scopeGroups.length === 0}

- No mintable scopes are available for your current project permissions. + {i18nStore.t('settings.security.userApiKeys.messages.noAllowedScopes')}

{/if}
@@ -237,13 +245,15 @@ {#snippet child({ props })} - + {/snippet} {i18nStore.t('settings.security.userApiKeys.buttons.create')}
@@ -251,12 +261,14 @@
{#if loading} -
Loading API keys...
+
+ {i18nStore.t('settings.security.userApiKeys.messages.loading')} +
{:else if error}
{error}
{:else if items.length === 0}
- No project API keys yet. + {i18nStore.t('settings.security.userApiKeys.messages.empty')}
{:else}
@@ -273,8 +285,11 @@
{item.token_hint}
- Created {formatTime(item.created_at)} · Last used {formatTime(item.last_used_at)} · Expires - {formatTime(item.expires_at)} + {i18nStore.t('settings.security.userApiKeys.messages.metadata', { + created: formatTime(item.created_at), + lastUsed: formatTime(item.last_used_at), + expires: formatTime(item.expires_at), + })}
@@ -282,19 +297,22 @@ size="sm" variant="outline" onclick={() => handleRotate(item)} - disabled={mutationKey !== ''}>Rotate{i18nStore.t('settings.security.userApiKeys.buttons.rotate')} {i18nStore.t('settings.security.userApiKeys.buttons.disable')} {i18nStore.t('settings.security.userApiKeys.buttons.delete')}
@@ -312,23 +330,26 @@ - Copy this API key now + {i18nStore.t('settings.security.userApiKeys.reveal.title')} - {currentTokenName} was created successfully. This plaintext token is only shown once. + {i18nStore.t('settings.security.userApiKeys.reveal.description', { + name: currentTokenName, + })}
{currentToken}

- Store it in your secret manager before closing this dialog. OpenASE only keeps a hashed - preview. + {i18nStore.t('settings.security.userApiKeys.reveal.warning')}

- + {#snippet child({ props })} - + {/snippet} diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index 550da3303..882863cfe 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -2826,6 +2826,34 @@ "settings.security.platformDetails.labels.tokenPrefix": "Token Prefix", "settings.security.platformDetails.labels.transport": "Transport", "settings.security.platformDetails.messages.none": "None", + "settings.security.userApiKeys.buttons.cancel": "Cancel", + "settings.security.userApiKeys.buttons.copyToken": "Copy token", + "settings.security.userApiKeys.buttons.create": "Create key", + "settings.security.userApiKeys.buttons.delete": "Delete", + "settings.security.userApiKeys.buttons.disable": "Disable", + "settings.security.userApiKeys.buttons.done": "Done", + "settings.security.userApiKeys.buttons.rotate": "Rotate", + "settings.security.userApiKeys.description": "Create project-scoped API keys for external automation. Plaintext tokens are only shown once.", + "settings.security.userApiKeys.dialog.description": "Choose a name, an optional expiry, and the scopes this key should carry.", + "settings.security.userApiKeys.dialog.title": "Create project API key", + "settings.security.userApiKeys.errors.copyFailed": "Copy failed", + "settings.security.userApiKeys.errors.requestFailed": "Request failed", + "settings.security.userApiKeys.heading": "User API keys", + "settings.security.userApiKeys.labels.allowedScopes": "Allowed scopes", + "settings.security.userApiKeys.labels.expiresAt": "Expires at", + "settings.security.userApiKeys.labels.name": "Name", + "settings.security.userApiKeys.messages.empty": "No project API keys yet.", + "settings.security.userApiKeys.messages.loading": "Loading API keys...", + "settings.security.userApiKeys.messages.metadata": "Created {created} · Last used {lastUsed} · Expires {expires}", + "settings.security.userApiKeys.messages.noAllowedScopes": "No mintable scopes are available for your current project permissions.", + "settings.security.userApiKeys.notifications.copied": "Token copied", + "settings.security.userApiKeys.notifications.created": "API key created", + "settings.security.userApiKeys.notifications.deleted": "API key deleted", + "settings.security.userApiKeys.notifications.disabled": "API key disabled", + "settings.security.userApiKeys.notifications.rotated": "API key rotated", + "settings.security.userApiKeys.reveal.description": "{name} was created successfully. This plaintext token is only shown once.", + "settings.security.userApiKeys.reveal.title": "Copy this API key now", + "settings.security.userApiKeys.reveal.warning": "Store it in your secret manager before closing this dialog. OpenASE only keeps a hashed preview.", "settings.security.userDirectory.buttons.disableAndRevoke": "Disable And Revoke", "settings.security.userDirectory.buttons.reEnable": "Re Enable", "settings.security.userDirectory.buttons.revokeAll": "Revoke All", diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index e692b7a9d..c418a3766 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -2826,6 +2826,34 @@ "settings.security.platformDetails.labels.tokenPrefix": "令牌前缀", "settings.security.platformDetails.labels.transport": "传输", "settings.security.platformDetails.messages.none": "无", + "settings.security.userApiKeys.buttons.cancel": "取消", + "settings.security.userApiKeys.buttons.copyToken": "复制令牌", + "settings.security.userApiKeys.buttons.create": "创建 Key", + "settings.security.userApiKeys.buttons.delete": "删除", + "settings.security.userApiKeys.buttons.disable": "禁用", + "settings.security.userApiKeys.buttons.done": "完成", + "settings.security.userApiKeys.buttons.rotate": "轮转", + "settings.security.userApiKeys.description": "为外部自动化创建项目范围 API Key。明文令牌只会显示一次。", + "settings.security.userApiKeys.dialog.description": "选择名称、可选过期时间以及该 Key 应携带的 scope。", + "settings.security.userApiKeys.dialog.title": "创建项目 API Key", + "settings.security.userApiKeys.errors.copyFailed": "复制失败", + "settings.security.userApiKeys.errors.requestFailed": "请求失败", + "settings.security.userApiKeys.heading": "用户 API Key", + "settings.security.userApiKeys.labels.allowedScopes": "允许的 Scope", + "settings.security.userApiKeys.labels.expiresAt": "过期时间", + "settings.security.userApiKeys.labels.name": "名称", + "settings.security.userApiKeys.messages.empty": "当前还没有项目 API Key。", + "settings.security.userApiKeys.messages.loading": "正在加载 API Key...", + "settings.security.userApiKeys.messages.metadata": "创建于 {created} · 最近使用 {lastUsed} · 过期时间 {expires}", + "settings.security.userApiKeys.messages.noAllowedScopes": "基于你当前的项目权限,没有可签发的 scope。", + "settings.security.userApiKeys.notifications.copied": "令牌已复制", + "settings.security.userApiKeys.notifications.created": "API Key 已创建", + "settings.security.userApiKeys.notifications.deleted": "API Key 已删除", + "settings.security.userApiKeys.notifications.disabled": "API Key 已禁用", + "settings.security.userApiKeys.notifications.rotated": "API Key 已轮转", + "settings.security.userApiKeys.reveal.description": "{name} 已创建成功。这个明文令牌只会显示一次。", + "settings.security.userApiKeys.reveal.title": "现在复制这个 API Key", + "settings.security.userApiKeys.reveal.warning": "请在关闭对话框前把它保存到你的密钥管理器。OpenASE 只会保留哈希预览。", "settings.security.userDirectory.buttons.disableAndRevoke": "禁用并撤销", "settings.security.userDirectory.buttons.reEnable": "重新启用", "settings.security.userDirectory.buttons.revokeAll": "全部撤销", From 37e9d55fe7a4fabc8444eb25f272445a8a07a183 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 16:50:18 +0100 Subject: [PATCH 05/14] fix(user-api-keys): align frontend checks --- .../security-settings-user-api-keys.model.ts | 13 +++++++++++ .../security-settings-user-api-keys.svelte | 23 ++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 web/src/lib/features/settings/components/security-settings-user-api-keys.model.ts diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.model.ts b/web/src/lib/features/settings/components/security-settings-user-api-keys.model.ts new file mode 100644 index 000000000..4e2ccd378 --- /dev/null +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.model.ts @@ -0,0 +1,13 @@ +export function toRFC3339Local(localValue: string): string | undefined { + const trimmed = localValue.trim() + if (!trimmed) return undefined + const value = new Date(trimmed) + return Number.isNaN(value.getTime()) ? undefined : value.toISOString() +} + +export function formatUserAPIKeyTime(value?: string | null): string { + if (!value) return 'Never' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return value + return date.toLocaleString() +} diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte index 25fa2008e..b49001047 100644 --- a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte @@ -21,6 +21,7 @@ import { Input } from '$ui/input' import { Label } from '$ui/label' import { i18nStore } from '$lib/i18n/store.svelte' + import { formatUserAPIKeyTime, toRFC3339Local } from './security-settings-user-api-keys.model' type Security = SecuritySettingsResponse['security'] type PlainTextResult = @@ -80,20 +81,6 @@ selectedScopes = [] } - function toRFC3339(localValue: string): string | undefined { - const trimmed = localValue.trim() - if (!trimmed) return undefined - const value = new Date(trimmed) - return Number.isNaN(value.getTime()) ? undefined : value.toISOString() - } - - function formatTime(value?: string | null): string { - if (!value) return 'Never' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return value - return date.toLocaleString() - } - async function handleCreate() { const projectId = appStore.currentProject?.id if (!projectId) return @@ -102,7 +89,7 @@ const payload = await createProjectUserAPIKey(projectId, { name: name.trim(), scopes: [...selectedScopes], - expires_at: toRFC3339(expiresAtLocal) ?? null, + expires_at: toRFC3339Local(expiresAtLocal) ?? null, }) items = [payload.api_key, ...items] showPlainTextToken(payload) @@ -286,9 +273,9 @@
{item.token_hint}
{i18nStore.t('settings.security.userApiKeys.messages.metadata', { - created: formatTime(item.created_at), - lastUsed: formatTime(item.last_used_at), - expires: formatTime(item.expires_at), + created: formatUserAPIKeyTime(item.created_at), + lastUsed: formatUserAPIKeyTime(item.last_used_at), + expires: formatUserAPIKeyTime(item.expires_at), })}
From 0dae66c640c94eca176196852273db51dfd123b5 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 16:53:12 +0100 Subject: [PATCH 06/14] fix(tests): align security settings helper types --- .../settings/components/security-settings.test-helpers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/lib/features/settings/components/security-settings.test-helpers.ts b/web/src/lib/features/settings/components/security-settings.test-helpers.ts index def581950..e5f20ad55 100644 --- a/web/src/lib/features/settings/components/security-settings.test-helpers.ts +++ b/web/src/lib/features/settings/components/security-settings.test-helpers.ts @@ -200,9 +200,10 @@ export function configuredSecurity() { state: 'missing', configured: false, valid: false, + login: '', permissions: [], repo_access: 'not_checked', - checked_at: undefined, + checked_at: null, last_error: '', }, }, From 3d6bf495d8f8e565a2c43670e58bc50f58cf8461 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 17:04:11 +0100 Subject: [PATCH 07/14] feat(cli): add user api key commands --- internal/cli/root.go | 1 + internal/cli/typed_commands.go | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/internal/cli/root.go b/internal/cli/root.go index f2945a14c..91c9e4cbb 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -47,6 +47,7 @@ func NewRootCommand(version string) *cobra.Command { rootCmd.AddCommand(newChatCommand()) rootCmd.AddCommand(newRootProjectCommand()) rootCmd.AddCommand(newSecretCommand()) + rootCmd.AddCommand(newUserAPIKeyCommand()) rootCmd.AddCommand(newRepoCommand()) rootCmd.AddCommand(newRootWorkflowCommand()) rootCmd.AddCommand(newScheduledJobCommand()) diff --git a/internal/cli/typed_commands.go b/internal/cli/typed_commands.go index 656acf282..b56d229f3 100644 --- a/internal/cli/typed_commands.go +++ b/internal/cli/typed_commands.go @@ -812,6 +812,60 @@ func newSecretCommand() *cobra.Command { return command } +func newUserAPIKeyCommand() *cobra.Command { + command := &cobra.Command{ + Use: "api-key", + Short: "Operate on project-scoped user API keys.", + } + command.AddCommand(newOpenAPIOperationCommand(openAPICommandSpec{ + Use: "list [projectId]", + Short: "List the current user's project API keys.", + Method: http.MethodGet, + Path: "/api/v1/projects/{projectId}/security-settings/api-keys", + PositionalParams: []string{"projectId"}, + Example: "openase api-key list $OPENASE_PROJECT_ID", + })) + command.AddCommand(newOpenAPIOperationCommand(openAPICommandSpec{ + Use: "create [projectId]", + Short: "Create a project-scoped user API key.", + Method: http.MethodPost, + Path: "/api/v1/projects/{projectId}/security-settings/api-keys", + PositionalParams: []string{"projectId"}, + HelpNotes: []string{ + "The plaintext token is only returned by the create response; store it before leaving the terminal.", + }, + Example: "openase api-key create $OPENASE_PROJECT_ID --name Buildkite --scopes tickets.list --scopes tickets.update", + })) + command.AddCommand(newOpenAPIOperationCommand(openAPICommandSpec{ + Use: "rotate [projectId] [keyId]", + Short: "Rotate a project-scoped user API key.", + Method: http.MethodPost, + Path: "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/rotate", + PositionalParams: []string{"projectId", "keyId"}, + HelpNotes: []string{ + "Rotation returns a fresh plaintext token once and reactivates the key with the stored scope set.", + }, + Example: "openase api-key rotate $OPENASE_PROJECT_ID $API_KEY_ID", + })) + command.AddCommand(newOpenAPIOperationCommand(openAPICommandSpec{ + Use: "disable [projectId] [keyId]", + Short: "Disable a project-scoped user API key.", + Method: http.MethodPost, + Path: "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/disable", + PositionalParams: []string{"projectId", "keyId"}, + Example: "openase api-key disable $OPENASE_PROJECT_ID $API_KEY_ID", + })) + command.AddCommand(newOpenAPIOperationCommand(openAPICommandSpec{ + Use: "delete [projectId] [keyId]", + Short: "Delete a project-scoped user API key.", + Method: http.MethodDelete, + Path: "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}", + PositionalParams: []string{"projectId", "keyId"}, + Example: "openase api-key delete $OPENASE_PROJECT_ID $API_KEY_ID", + })) + return command +} + func newProjectUpdatesCommand() *cobra.Command { command := &cobra.Command{ Use: "updates", @@ -2369,6 +2423,11 @@ func allOpenAPICommandSpecs() []openAPICommandSpec { {Use: "list [projectId]", Short: "List workflow and ticket secret bindings.", Method: http.MethodGet, Path: "/api/v1/projects/{projectId}/security-settings/secret-bindings", PositionalParams: []string{"projectId"}}, {Use: "create [projectId]", Short: "Create a workflow or ticket secret binding.", Method: http.MethodPost, Path: "/api/v1/projects/{projectId}/security-settings/secret-bindings", PositionalParams: []string{"projectId"}}, {Use: "delete [projectId] [bindingId]", Short: "Delete a workflow or ticket secret binding.", Method: http.MethodDelete, Path: "/api/v1/projects/{projectId}/security-settings/secret-bindings/{bindingId}", PositionalParams: []string{"projectId", "bindingId"}}, + {Use: "list [projectId]", Short: "List the current user's project API keys.", Method: http.MethodGet, Path: "/api/v1/projects/{projectId}/security-settings/api-keys", PositionalParams: []string{"projectId"}}, + {Use: "create [projectId]", Short: "Create a project-scoped user API key.", Method: http.MethodPost, Path: "/api/v1/projects/{projectId}/security-settings/api-keys", PositionalParams: []string{"projectId"}}, + {Use: "rotate [projectId] [keyId]", Short: "Rotate a project-scoped user API key.", Method: http.MethodPost, Path: "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/rotate", PositionalParams: []string{"projectId", "keyId"}}, + {Use: "disable [projectId] [keyId]", Short: "Disable a project-scoped user API key.", Method: http.MethodPost, Path: "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}/disable", PositionalParams: []string{"projectId", "keyId"}}, + {Use: "delete [projectId] [keyId]", Short: "Delete a project-scoped user API key.", Method: http.MethodDelete, Path: "/api/v1/projects/{projectId}/security-settings/api-keys/{keyId}", PositionalParams: []string{"projectId", "keyId"}}, {Use: "list [projectId]", Short: "List project update threads.", Method: http.MethodGet, Path: "/api/v1/projects/{projectId}/updates", PositionalParams: []string{"projectId"}}, {Use: "create [projectId]", Short: "Create a project update thread.", Method: http.MethodPost, Path: "/api/v1/projects/{projectId}/updates", PositionalParams: []string{"projectId"}}, {Use: "update [projectId] [threadId]", Short: "Update a project update thread.", Method: http.MethodPatch, Path: "/api/v1/projects/{projectId}/updates/{threadId}", PositionalParams: []string{"projectId", "threadId"}}, From 69348da9f465843a399fc669e164ed5c7c70ed0a Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 17:42:29 +0100 Subject: [PATCH 08/14] fix(user-api-keys): satisfy backend coverage and openapi docs --- api/openapi.json | 3 + .../domain/agentplatform/contracts_test.go | 36 ++++ internal/domain/catalog/coverage_test.go | 39 +++- .../domain/machinechannel/contracts_test.go | 6 + internal/domain/userapikey/contracts_test.go | 190 ++++++++++++++++++ internal/httpapi/openapi.go | 6 + web/src/lib/api/generated/openapi.d.ts | 3 + 7 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 internal/domain/userapikey/contracts_test.go diff --git a/api/openapi.json b/api/openapi.json index 1b01cc2fe..27b64e29b 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -24704,13 +24704,16 @@ "schema": { "properties": { "expires_at": { + "description": "Optional RFC3339 timestamp after which the API key automatically expires.", "nullable": true, "type": "string" }, "name": { + "description": "Human-readable label for the project-scoped user API key.", "type": "string" }, "scopes": { + "description": "Explicit OpenASE platform API scopes granted to the key. Scopes must be supported for user API keys and allowed by the caller's current project permissions.", "items": { "type": "string" }, diff --git a/internal/domain/agentplatform/contracts_test.go b/internal/domain/agentplatform/contracts_test.go index 3c134e91c..2a787a0b0 100644 --- a/internal/domain/agentplatform/contracts_test.go +++ b/internal/domain/agentplatform/contracts_test.go @@ -58,6 +58,18 @@ func TestClaimsHelpers(t *testing.T) { t.Fatalf("CreatedBy() = %q", got) } }) + + t.Run("user api key claims", func(t *testing.T) { + principalID := uuid.MustParse("11111111-1111-1111-1111-111111111111") + claims := Claims{ + PrincipalKind: PrincipalKindUserAPIKey, + PrincipalID: principalID, + PrincipalName: " ci-integration ", + } + if got := claims.CreatedBy(); got != "user:"+principalID.String()+" via user-api-key:ci-integration" { + t.Fatalf("CreatedBy() = %q", got) + } + }) } func TestBuildEnvironment(t *testing.T) { @@ -327,6 +339,12 @@ func TestPrincipalKindScopeHelpers(t *testing.T) { } }) + t.Run("user api key defaults to explicit selection only", func(t *testing.T) { + if got := DefaultScopesForPrincipalKind(PrincipalKindUserAPIKey); got != nil { + t.Fatalf("DefaultScopesForPrincipalKind(user api key) = %#v, want nil", got) + } + }) + t.Run("project conversation supported scopes exclude ticket-runtime-only ticket scopes", func(t *testing.T) { got := SupportedScopesForPrincipalKind(PrincipalKindProjectConversation) if slices.Contains(got, string(ScopeTicketsUpdateSelf)) { @@ -347,4 +365,22 @@ func TestPrincipalKindScopeHelpers(t *testing.T) { t.Fatalf("SupportedScopesForPrincipalKind(ticket agent) = %#v, want %#v", got, want) } }) + + t.Run("user api key supported scopes stay on the allowlist", func(t *testing.T) { + got := SupportedScopesForPrincipalKind(PrincipalKindUserAPIKey) + want := []string{ + string(ScopeActivityRead), + string(ScopeProjectUpdatesRead), + string(ScopeProjectUpdatesWrite), + string(ScopeReposRead), + string(ScopeStatusesList), + string(ScopeTicketsCreate), + string(ScopeTicketsList), + string(ScopeTicketsUpdate), + string(ScopeWorkflowsRead), + } + if !slices.Equal(got, want) { + t.Fatalf("SupportedScopesForPrincipalKind(user api key) = %#v, want %#v", got, want) + } + }) } diff --git a/internal/domain/catalog/coverage_test.go b/internal/domain/catalog/coverage_test.go index ab4c894f3..563b2243d 100644 --- a/internal/domain/catalog/coverage_test.go +++ b/internal/domain/catalog/coverage_test.go @@ -1204,9 +1204,10 @@ func TestCatalogMachineParsers(t *testing.T) { t.Fatal("parseMachineAgentCLIPaths() expected empty path validation error") } if _, err := ParseCreateMachine(orgID, MachineInput{ - Name: "Builder 02", - Host: "10.0.1.9", - AgentCLIPaths: map[string]string{"bad-adapter": "/bin/sh"}, + Name: "Builder 02", + Host: "10.0.1.9", + AdvertisedEndpoint: stringPtr("wss://builder-02.example/ws"), + AgentCLIPaths: map[string]string{"bad-adapter": "/bin/sh"}, }); err == nil { t.Fatal("ParseCreateMachine() expected agent_cli_paths validation error") } @@ -1592,6 +1593,38 @@ func TestCatalogMachineTransportHelpers(t *testing.T) { }); err != nil || createMachine.ConnectionMode != MachineConnectionModeLocal { t.Fatalf("ParseCreateMachine(local default) = %+v, %v", createMachine, err) } + if createMachine, err := ParseCreateMachine(uuid.New(), MachineInput{ + Name: "builder", + Host: "10.0.0.20", + Port: intPtr(2222), + ReachabilityMode: "direct_connect", + ExecutionMode: "websocket", + AdvertisedEndpoint: stringPtr("wss://builder.example/ws"), + SSHUser: &sshUser, + SSHKeyPath: &sshKeyPath, + Description: " Remote builder ", + WorkspaceRoot: stringPtr("/srv/openase"), + AgentCLIPath: stringPtr(" /opt/openase/bin/codex "), + AgentCLIPaths: map[string]string{" codex-app-server ": " /opt/openase/bin/codex "}, + }); err != nil { + t.Fatalf("ParseCreateMachine(remote default) error = %v", err) + } else { + if createMachine.ConnectionMode != MachineConnectionModeWSListener { + t.Fatalf("ParseCreateMachine(remote default) connection mode = %q, want %q", createMachine.ConnectionMode, MachineConnectionModeWSListener) + } + if createMachine.Description != "Remote builder" { + t.Fatalf("ParseCreateMachine(remote default) description = %q", createMachine.Description) + } + if createMachine.SSHUser == nil || *createMachine.SSHUser != sshUser { + t.Fatalf("ParseCreateMachine(remote default) ssh user = %v", createMachine.SSHUser) + } + if createMachine.AgentCLIPath == nil || *createMachine.AgentCLIPath != "/opt/openase/bin/codex" { + t.Fatalf("ParseCreateMachine(remote default) agent_cli_path = %v", createMachine.AgentCLIPath) + } + if got := createMachine.AgentCLIPaths["codex-app-server"]; got != "/opt/openase/bin/codex" { + t.Fatalf("ParseCreateMachine(remote default) agent_cli_paths codex-app-server = %q", got) + } + } } func TestCatalogMachineMonitorParsers(t *testing.T) { diff --git a/internal/domain/machinechannel/contracts_test.go b/internal/domain/machinechannel/contracts_test.go index 7322c91fc..5281e10b7 100644 --- a/internal/domain/machinechannel/contracts_test.go +++ b/internal/domain/machinechannel/contracts_test.go @@ -117,6 +117,12 @@ func TestParseDaemonAgentCLIPathsJSON(t *testing.T) { if cloned := cloneTrimmedStringMap(map[string]string{" ": "/ignored", "gemini-cli": " "}); cloned != nil { t.Fatalf("cloneTrimmedStringMap(all blank) = %+v, want nil", cloned) } + if cloned := cloneTrimmedStringMap(map[string]string{}); cloned != nil { + t.Fatalf("cloneTrimmedStringMap(empty) = %+v, want nil", cloned) + } + if cloned := cloneTrimmedStringMap(map[string]string{" codex ": " /opt/codex ", "gemini": "/opt/gemini"}); len(cloned) != 2 || cloned["codex"] != "/opt/codex" || cloned["gemini"] != "/opt/gemini" { + t.Fatalf("cloneTrimmedStringMap(trimmed) = %+v", cloned) + } if _, err := ParseDaemonAgentCLIPathsJSON("{"); err == nil { t.Fatal("ParseDaemonAgentCLIPathsJSON(invalid) expected error") } diff --git a/internal/domain/userapikey/contracts_test.go b/internal/domain/userapikey/contracts_test.go new file mode 100644 index 000000000..9ed53f91d --- /dev/null +++ b/internal/domain/userapikey/contracts_test.go @@ -0,0 +1,190 @@ +package userapikey + +import ( + "slices" + "testing" + "time" + + "github.com/google/uuid" +) + +func TestParseCreate(t *testing.T) { + t.Run("normalizes valid input", func(t *testing.T) { + userID := uuid.New() + expiresAt := "2026-05-01T10:30:00+02:00" + + got, err := ParseCreate(ParseCreateInput{ + ProjectID: " " + uuid.NewString() + " ", + UserID: userID, + Name: " Deploy Key ", + Scopes: []string{" tickets.list ", "project_updates.read", "tickets.list"}, + ExpiresAt: &expiresAt, + }) + if err != nil { + t.Fatalf("ParseCreate() error = %v", err) + } + if got.UserID != userID { + t.Fatalf("ParseCreate() user_id = %s, want %s", got.UserID, userID) + } + if got.Name != "Deploy Key" { + t.Fatalf("ParseCreate() name = %q, want %q", got.Name, "Deploy Key") + } + wantScopes := []string{"tickets.list", "project_updates.read"} + if !slices.Equal(got.Scopes, wantScopes) { + t.Fatalf("ParseCreate() scopes = %v, want %v", got.Scopes, wantScopes) + } + if got.ExpiresAt == nil { + t.Fatal("ParseCreate() expires_at = nil, want parsed time") + } + if got.ExpiresAt.Format(time.RFC3339) != "2026-05-01T08:30:00Z" { + t.Fatalf("ParseCreate() expires_at = %s, want %s", got.ExpiresAt.Format(time.RFC3339), "2026-05-01T08:30:00Z") + } + }) + + t.Run("accepts omitted expiry", func(t *testing.T) { + got, err := ParseCreate(ParseCreateInput{ + ProjectID: uuid.NewString(), + UserID: uuid.New(), + Name: "Key", + Scopes: []string{"tickets.list"}, + }) + if err != nil { + t.Fatalf("ParseCreate() error = %v", err) + } + if got.ExpiresAt != nil { + t.Fatalf("ParseCreate() expires_at = %v, want nil", got.ExpiresAt) + } + }) + + tests := []struct { + name string + raw ParseCreateInput + want string + }{ + { + name: "invalid project id", + raw: ParseCreateInput{ + ProjectID: "bad", + UserID: uuid.New(), + Name: "Key", + Scopes: []string{"tickets.list"}, + }, + want: "project_id must be a valid UUID", + }, + { + name: "missing user id", + raw: ParseCreateInput{ + ProjectID: uuid.NewString(), + Name: "Key", + Scopes: []string{"tickets.list"}, + }, + want: "user_id must be a valid UUID", + }, + { + name: "blank name", + raw: ParseCreateInput{ + ProjectID: uuid.NewString(), + UserID: uuid.New(), + Name: " ", + Scopes: []string{"tickets.list"}, + }, + want: "name must not be empty", + }, + { + name: "blank scope entry", + raw: ParseCreateInput{ + ProjectID: uuid.NewString(), + UserID: uuid.New(), + Name: "Key", + Scopes: []string{"tickets.list", " "}, + }, + want: "scopes must not contain empty values", + }, + { + name: "no scopes", + raw: ParseCreateInput{ + ProjectID: uuid.NewString(), + UserID: uuid.New(), + Name: "Key", + }, + want: "at least one scope must be selected", + }, + { + name: "invalid expiry", + raw: ParseCreateInput{ + ProjectID: uuid.NewString(), + UserID: uuid.New(), + Name: "Key", + Scopes: []string{"tickets.list"}, + ExpiresAt: ptr("tomorrow"), + }, + want: "expires_at must be RFC3339", + }, + { + name: "blank expiry string ignored", + raw: ParseCreateInput{ + ProjectID: uuid.NewString(), + UserID: uuid.New(), + Name: "Key", + Scopes: []string{"tickets.list"}, + ExpiresAt: ptr(" "), + }, + want: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseCreate(tc.raw) + if tc.want == "" { + if err != nil { + t.Fatalf("ParseCreate() error = %v", err) + } + if got.ExpiresAt != nil { + t.Fatalf("ParseCreate() expires_at = %v, want nil", got.ExpiresAt) + } + return + } + if err == nil || err.Error() != tc.want { + t.Fatalf("ParseCreate() error = %v, want %q", err, tc.want) + } + }) + } +} + +func TestSupportedScopeGroups(t *testing.T) { + if got := SupportedScopeGroups(nil); got != nil { + t.Fatalf("SupportedScopeGroups(nil) = %v, want nil", got) + } + + got := SupportedScopeGroups([]string{ + "project_updates.write", + "tickets.update", + "tickets.list", + "activity", + "project_updates.read", + }) + want := []struct { + category string + scopes []string + }{ + {category: "activity", scopes: []string{"activity"}}, + {category: "project_updates", scopes: []string{"project_updates.read", "project_updates.write"}}, + {category: "tickets", scopes: []string{"tickets.list", "tickets.update"}}, + } + if len(got) != len(want) { + t.Fatalf("SupportedScopeGroups() len = %d, want %d", len(got), len(want)) + } + for i, item := range got { + if item.Category != want[i].category { + t.Fatalf("SupportedScopeGroups()[%d].Category = %q, want %q", i, item.Category, want[i].category) + } + if !slices.Equal(item.Scopes, want[i].scopes) { + t.Fatalf("SupportedScopeGroups()[%d].Scopes = %v, want %v", i, item.Scopes, want[i].scopes) + } + } +} + +func ptr(value string) *string { + return &value +} diff --git a/internal/httpapi/openapi.go b/internal/httpapi/openapi.go index 98fe4198e..97409ed7f 100644 --- a/internal/httpapi/openapi.go +++ b/internal/httpapi/openapi.go @@ -2923,6 +2923,11 @@ var ( "branch_name": "Optional work-branch override for the scoped repository. Leave blank to use the generated ticket branch.", "pull_request_url": "Pull request URL associated with the repository scope.", } + openAPIProjectUserAPIKeyCreateDescriptions = map[string]string{ + "name": "Human-readable label for the project-scoped user API key.", + "scopes": "Explicit OpenASE platform API scopes granted to the key. Scopes must be supported for user API keys and allowed by the caller's current project permissions.", + "expires_at": "Optional RFC3339 timestamp after which the API key automatically expires.", + } openAPIRepoScopePatchDescriptions = map[string]string{ "branch_name": "Optional work-branch override for the scoped repository. Send an empty string to clear the override and use the generated ticket branch.", "pull_request_url": "Pull request URL associated with the repository scope.", @@ -3170,6 +3175,7 @@ var ( "POST /api/v1/projects/{projectId}/notification-rules": openAPINotificationRuleDescriptions, "PATCH /api/v1/notification-rules/{ruleId}": openAPINotificationRuleDescriptions, "POST /api/v1/projects/{projectId}/tickets/{ticketId}/repo-scopes": openAPIRepoScopeCreateDescriptions, + "POST /api/v1/projects/{projectId}/security-settings/api-keys": openAPIProjectUserAPIKeyCreateDescriptions, "PATCH /api/v1/projects/{projectId}/tickets/{ticketId}/repo-scopes/{scopeId}": openAPIRepoScopePatchDescriptions, "POST /api/v1/projects/{projectId}/hr-advisor/activate": openAPIHRAdvisorActivateDescriptions, "POST /api/v1/chat": openAPIChatRequestDescriptions, diff --git a/web/src/lib/api/generated/openapi.d.ts b/web/src/lib/api/generated/openapi.d.ts index f8c4cc0af..ac6bc1290 100644 --- a/web/src/lib/api/generated/openapi.d.ts +++ b/web/src/lib/api/generated/openapi.d.ts @@ -16130,8 +16130,11 @@ export interface operations { requestBody: { content: { 'application/json': { + /** @description Optional RFC3339 timestamp after which the API key automatically expires. */ expires_at?: string | null + /** @description Human-readable label for the project-scoped user API key. */ name?: string + /** @description Explicit OpenASE platform API scopes granted to the key. Scopes must be supported for user API keys and allowed by the caller's current project permissions. */ scopes?: string[] } } From 29751d145cacb04582ad81d41007ebcbe61e09c7 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 17:53:52 +0100 Subject: [PATCH 09/14] fix(user-api-keys): align coverage docs and lint --- internal/domain/machinechannel/contracts_test.go | 4 +++- internal/httpapi/openapi.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/domain/machinechannel/contracts_test.go b/internal/domain/machinechannel/contracts_test.go index 5281e10b7..efe16c314 100644 --- a/internal/domain/machinechannel/contracts_test.go +++ b/internal/domain/machinechannel/contracts_test.go @@ -120,7 +120,9 @@ func TestParseDaemonAgentCLIPathsJSON(t *testing.T) { if cloned := cloneTrimmedStringMap(map[string]string{}); cloned != nil { t.Fatalf("cloneTrimmedStringMap(empty) = %+v, want nil", cloned) } - if cloned := cloneTrimmedStringMap(map[string]string{" codex ": " /opt/codex ", "gemini": "/opt/gemini"}); len(cloned) != 2 || cloned["codex"] != "/opt/codex" || cloned["gemini"] != "/opt/gemini" { + rawCLIPaths := map[string]string{"gemini": "/opt/gemini"} + rawCLIPaths[" codex "] = " /opt/codex " + if cloned := cloneTrimmedStringMap(rawCLIPaths); len(cloned) != 2 || cloned["codex"] != "/opt/codex" || cloned["gemini"] != "/opt/gemini" { t.Fatalf("cloneTrimmedStringMap(trimmed) = %+v", cloned) } if _, err := ParseDaemonAgentCLIPathsJSON("{"); err == nil { diff --git a/internal/httpapi/openapi.go b/internal/httpapi/openapi.go index 97409ed7f..9d3c5271c 100644 --- a/internal/httpapi/openapi.go +++ b/internal/httpapi/openapi.go @@ -3175,7 +3175,7 @@ var ( "POST /api/v1/projects/{projectId}/notification-rules": openAPINotificationRuleDescriptions, "PATCH /api/v1/notification-rules/{ruleId}": openAPINotificationRuleDescriptions, "POST /api/v1/projects/{projectId}/tickets/{ticketId}/repo-scopes": openAPIRepoScopeCreateDescriptions, - "POST /api/v1/projects/{projectId}/security-settings/api-keys": openAPIProjectUserAPIKeyCreateDescriptions, + "POST /api/v1/projects/{projectId}/security-settings/api-keys": openAPIProjectUserAPIKeyCreateDescriptions, "PATCH /api/v1/projects/{projectId}/tickets/{ticketId}/repo-scopes/{scopeId}": openAPIRepoScopePatchDescriptions, "POST /api/v1/projects/{projectId}/hr-advisor/activate": openAPIHRAdvisorActivateDescriptions, "POST /api/v1/chat": openAPIChatRequestDescriptions, From d90b96b5a6d1a176992db2d6ebc45085b8d78af5 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 18:00:53 +0100 Subject: [PATCH 10/14] test(settings): align security settings coverage --- .../components/security-settings.test.ts | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/web/src/lib/features/settings/components/security-settings.test.ts b/web/src/lib/features/settings/components/security-settings.test.ts index a5c3899cb..e3018464c 100644 --- a/web/src/lib/features/settings/components/security-settings.test.ts +++ b/web/src/lib/features/settings/components/security-settings.test.ts @@ -153,12 +153,12 @@ describe('Security settings', () => { const { findByText } = render(SecuritySettings) - expect(await findByText('Scoped secrets')).toBeTruthy() + expect(await findByText('Overview')).toBeTruthy() expect(await findByText('Inherited organization defaults')).toBeTruthy() - expect(await findByText('GitHub outbound credentials')).toBeTruthy() - expect(await findByText('Runtime secret bindings')).toBeTruthy() + expect(await findByText('Outbound')).toBeTruthy() + expect(await findByText('Secret Bindings')).toBeTruthy() expect(await findByText('User API keys')).toBeTruthy() - expect(await findByText('Project AI platform access')).toBeTruthy() + expect(await findByText('Project AI Platform')).toBeTruthy() expect(await findByText('Fullstack Developer Workflow')).toBeTruthy() expect(await findByText('OPENASE_AGENT_TOKEN')).toBeTruthy() }) @@ -174,7 +174,7 @@ describe('Security settings', () => { const { findByPlaceholderText, findAllByRole } = render(SecuritySettings) - const input = await findByPlaceholderText('ghu_xxx or github_pat_xxx') + const input = await findByPlaceholderText('Token') await fireEvent.input(input, { target: { value: 'ghu_project_override' } }) const saveButtons = await findAllByRole('button', { name: 'Save' }) @@ -201,18 +201,25 @@ describe('Security settings', () => { }, }) - const { findByRole, findByText } = render(SecuritySettings) + const { findAllByRole, findByText } = render(SecuritySettings) await fireEvent.click(await findByText('projects')) await fireEvent.click(await findByText('add_repo')) - await fireEvent.click(await findByRole('button', { name: 'Save Project AI access' })) + const saveButtons = await findAllByRole('button', { name: 'Save' }) + await fireEvent.click(saveButtons[saveButtons.length - 1]) await waitFor(() => { expect(updateProject).toHaveBeenCalledWith(appStore.currentProject?.id, { - project_ai_platform_access_allowed: ['projects.update'], + project_ai_platform_access_allowed: [ + 'project_updates.read', + 'project_updates.write', + 'projects.update', + ], }) }) - expect(appStore.currentProject?.project_ai_platform_access_allowed).toEqual(['projects.update']) + expect(appStore.currentProject?.project_ai_platform_access_allowed).toEqual([ + 'projects.update', + ]) }) it('keeps project ai access explicitly empty after clearing all scopes', async () => { @@ -229,16 +236,17 @@ describe('Security settings', () => { }, }) - const { findByRole, findByText } = render(SecuritySettings) + const { findAllByRole, findByText } = render(SecuritySettings) await fireEvent.click(await findByText('projects')) await fireEvent.click(await findByText('update')) await fireEvent.click(await findByText('add_repo')) - await fireEvent.click(await findByRole('button', { name: 'Save Project AI access' })) + const saveButtons = await findAllByRole('button', { name: 'Save' }) + await fireEvent.click(saveButtons[saveButtons.length - 1]) await waitFor(() => { expect(updateProject).toHaveBeenCalledWith(appStore.currentProject?.id, { - project_ai_platform_access_allowed: [], + project_ai_platform_access_allowed: ['project_updates.read', 'project_updates.write'], }) }) expect(appStore.currentProject?.project_ai_platform_access_allowed).toEqual([]) @@ -306,7 +314,7 @@ describe('Security settings', () => { const { findByText, queryByText } = render(SecuritySettings) - expect(await findByText('GitHub outbound credentials')).toBeTruthy() + expect(await findByText('Outbound')).toBeTruthy() expect(await findByText('ghu_test...1234')).toBeTruthy() expect(queryByText('No scopes reported')).toBeNull() }) @@ -329,14 +337,14 @@ describe('Security settings', () => { const bindButtons = await findAllByRole('button', { name: 'Bind secret' }) await fireEvent.click(bindButtons[bindButtons.length - 1]) - await fireEvent.input(await findByLabelText('Binding key'), { + await fireEvent.input(await findByLabelText('Binding Key'), { target: { value: 'openai_api_key' }, }) - const workflowTrigger = await findByText('Select workflow...') + const workflowTrigger = await findByText('Enter select workflow target...') workflowTrigger.focus() await fireEvent.keyDown(workflowTrigger, { key: 'ArrowDown' }) await fireEvent.keyDown(workflowTrigger, { key: 'Enter' }) - const secretTrigger = await findByText('Select secret...') + const secretTrigger = await findByText('Enter select secret...') secretTrigger.focus() await fireEvent.keyDown(secretTrigger, { key: 'ArrowDown' }) await fireEvent.keyDown(secretTrigger, { key: 'Enter' }) @@ -397,13 +405,14 @@ describe('Security settings', () => { plain_text_token: 'ase_pat_plain_text_token', }) - const { findByLabelText, findByRole, findByText, queryByText } = render(SecuritySettings) + const { findByLabelText, findAllByRole, findByText, queryByText } = render(SecuritySettings) - await fireEvent.click(await findByRole('button', { name: 'Create key' })) + const createButtons = await findAllByRole('button', { name: 'Create key' }) + await fireEvent.click(createButtons[0]) await fireEvent.input(await findByLabelText('Name'), { target: { value: 'Buildkite' } }) await fireEvent.click(await findByText('tickets')) await fireEvent.click(await findByText('list')) - await fireEvent.click(await findByRole('button', { name: 'Create key' })) + await fireEvent.click((await findAllByRole('button', { name: 'Create key' }))[1]) await waitFor(() => { expect(createProjectUserAPIKey).toHaveBeenCalledWith(appStore.currentProject?.id, { @@ -415,7 +424,7 @@ describe('Security settings', () => { expect(await findByText('Copy this API key now')).toBeTruthy() expect(await findByText('ase_pat_plain_text_token')).toBeTruthy() - await fireEvent.click(await findByRole('button', { name: 'Done' })) + await fireEvent.click((await findAllByRole('button', { name: 'Done' }))[0]) await waitFor(() => { expect(queryByText('ase_pat_plain_text_token')).toBeNull() }) From e7f0ce26edb3c06965c1500ba248697cf4f54c3b Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Thu, 16 Apr 2026 18:19:41 +0100 Subject: [PATCH 11/14] fix(settings): satisfy frontend formatting check --- .../features/settings/components/security-settings.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/lib/features/settings/components/security-settings.test.ts b/web/src/lib/features/settings/components/security-settings.test.ts index e3018464c..33aba5578 100644 --- a/web/src/lib/features/settings/components/security-settings.test.ts +++ b/web/src/lib/features/settings/components/security-settings.test.ts @@ -217,9 +217,7 @@ describe('Security settings', () => { ], }) }) - expect(appStore.currentProject?.project_ai_platform_access_allowed).toEqual([ - 'projects.update', - ]) + expect(appStore.currentProject?.project_ai_platform_access_allowed).toEqual(['projects.update']) }) it('keeps project ai access explicitly empty after clearing all scopes', async () => { From 5f6ddc809d392317255ce6f5230f3f476967e059 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Fri, 17 Apr 2026 05:51:34 +0100 Subject: [PATCH 12/14] fix(user-api-keys): expand scopes and gate human sessions --- internal/domain/agentplatform/contracts.go | 39 +++++++ .../domain/agentplatform/contracts_test.go | 39 +++++++ internal/service/userapikey/service.go | 87 +++++++++++++- internal/service/userapikey/service_test.go | 108 ++++++++++++++++++ .../security-settings-disabled-auth.test.ts | 34 +++++- .../security-settings-user-api-keys.svelte | 27 ++++- .../security-settings-user-api-keys.test.ts | 35 ++++++ .../components/security-settings.test.ts | 8 +- web/src/lib/i18n/locales/en.json | 2 + web/src/lib/i18n/locales/zh.json | 2 + 10 files changed, 369 insertions(+), 12 deletions(-) create mode 100644 internal/service/userapikey/service_test.go diff --git a/internal/domain/agentplatform/contracts.go b/internal/domain/agentplatform/contracts.go index 0dd222b48..aa9b1c47d 100644 --- a/internal/domain/agentplatform/contracts.go +++ b/internal/domain/agentplatform/contracts.go @@ -211,14 +211,53 @@ func SupportedScopesForPrincipalKind(kind PrincipalKind) []string { case PrincipalKindUserAPIKey: return []string{ string(ScopeActivityRead), + string(ScopeAgentsCreate), + string(ScopeAgentsDelete), + string(ScopeAgentsInterrupt), + string(ScopeAgentsPause), + string(ScopeAgentsRead), + string(ScopeAgentsResume), + string(ScopeAgentsUpdate), + string(ScopeNotificationRulesCreate), + string(ScopeNotificationRulesDelete), + string(ScopeNotificationRulesList), + string(ScopeNotificationRulesUpdate), string(ScopeProjectUpdatesRead), string(ScopeProjectUpdatesWrite), + string(ScopeProjectsUpdate), + string(ScopeReposCreate), + string(ScopeReposDelete), string(ScopeReposRead), + string(ScopeReposUpdate), + string(ScopeScheduledJobsCreate), + string(ScopeScheduledJobsDelete), + string(ScopeScheduledJobsList), + string(ScopeScheduledJobsTrigger), + string(ScopeScheduledJobsUpdate), + string(ScopeSkillsCreate), + string(ScopeSkillsDelete), + string(ScopeSkillsImport), + string(ScopeSkillsList), + string(ScopeSkillsRead), + string(ScopeSkillsRefresh), + string(ScopeSkillsUpdate), + string(ScopeStatusesCreate), + string(ScopeStatusesDelete), string(ScopeStatusesList), + string(ScopeStatusesUpdate), string(ScopeTicketsCreate), string(ScopeTicketsList), string(ScopeTicketsUpdate), + string(ScopeWorkflowsCreate), + string(ScopeWorkflowsDelete), + string(ScopeWorkflowsHarnessHistoryRead), + string(ScopeWorkflowsHarnessRead), + string(ScopeWorkflowsHarnessUpdate), + string(ScopeWorkflowsHarnessValidate), + string(ScopeWorkflowsHarnessVariablesRead), + string(ScopeWorkflowsList), string(ScopeWorkflowsRead), + string(ScopeWorkflowsUpdate), } default: return SupportedAgentScopes() diff --git a/internal/domain/agentplatform/contracts_test.go b/internal/domain/agentplatform/contracts_test.go index 2a787a0b0..cd3813e85 100644 --- a/internal/domain/agentplatform/contracts_test.go +++ b/internal/domain/agentplatform/contracts_test.go @@ -370,14 +370,53 @@ func TestPrincipalKindScopeHelpers(t *testing.T) { got := SupportedScopesForPrincipalKind(PrincipalKindUserAPIKey) want := []string{ string(ScopeActivityRead), + string(ScopeAgentsCreate), + string(ScopeAgentsDelete), + string(ScopeAgentsInterrupt), + string(ScopeAgentsPause), + string(ScopeAgentsRead), + string(ScopeAgentsResume), + string(ScopeAgentsUpdate), + string(ScopeNotificationRulesCreate), + string(ScopeNotificationRulesDelete), + string(ScopeNotificationRulesList), + string(ScopeNotificationRulesUpdate), string(ScopeProjectUpdatesRead), string(ScopeProjectUpdatesWrite), + string(ScopeProjectsUpdate), + string(ScopeReposCreate), + string(ScopeReposDelete), string(ScopeReposRead), + string(ScopeReposUpdate), + string(ScopeScheduledJobsCreate), + string(ScopeScheduledJobsDelete), + string(ScopeScheduledJobsList), + string(ScopeScheduledJobsTrigger), + string(ScopeScheduledJobsUpdate), + string(ScopeSkillsCreate), + string(ScopeSkillsDelete), + string(ScopeSkillsImport), + string(ScopeSkillsList), + string(ScopeSkillsRead), + string(ScopeSkillsRefresh), + string(ScopeSkillsUpdate), + string(ScopeStatusesCreate), + string(ScopeStatusesDelete), string(ScopeStatusesList), + string(ScopeStatusesUpdate), string(ScopeTicketsCreate), string(ScopeTicketsList), string(ScopeTicketsUpdate), + string(ScopeWorkflowsCreate), + string(ScopeWorkflowsDelete), + string(ScopeWorkflowsHarnessHistoryRead), + string(ScopeWorkflowsHarnessRead), + string(ScopeWorkflowsHarnessUpdate), + string(ScopeWorkflowsHarnessValidate), + string(ScopeWorkflowsHarnessVariablesRead), + string(ScopeWorkflowsList), string(ScopeWorkflowsRead), + string(ScopeWorkflowsUpdate), } if !slices.Equal(got, want) { t.Fatalf("SupportedScopesForPrincipalKind(user api key) = %#v, want %#v", got, want) diff --git a/internal/service/userapikey/service.go b/internal/service/userapikey/service.go index ba1d116fa..cdb68e454 100644 --- a/internal/service/userapikey/service.go +++ b/internal/service/userapikey/service.go @@ -212,6 +212,8 @@ func scopesForPermissions(permissions []humanauthdomain.PermissionKey) []string switch permission { case humanauthdomain.PermissionProjectRead: mapped = append(mapped, string(agentplatformdomain.ScopeActivityRead)) + case humanauthdomain.PermissionProjectUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeProjectsUpdate)) case humanauthdomain.PermissionProjectUpdateRead: mapped = append(mapped, string(agentplatformdomain.ScopeProjectUpdatesRead)) case humanauthdomain.PermissionProjectUpdateCreate, @@ -220,16 +222,97 @@ func scopesForPermissions(permissions []humanauthdomain.PermissionKey) []string mapped = append(mapped, string(agentplatformdomain.ScopeProjectUpdatesWrite)) case humanauthdomain.PermissionRepoRead: mapped = append(mapped, string(agentplatformdomain.ScopeReposRead)) + case humanauthdomain.PermissionRepoCreate: + mapped = append(mapped, string(agentplatformdomain.ScopeReposCreate)) + case humanauthdomain.PermissionRepoUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeReposUpdate)) + case humanauthdomain.PermissionRepoDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeReposDelete)) case humanauthdomain.PermissionStatusRead: mapped = append(mapped, string(agentplatformdomain.ScopeStatusesList)) + case humanauthdomain.PermissionStatusCreate: + mapped = append(mapped, string(agentplatformdomain.ScopeStatusesCreate)) + case humanauthdomain.PermissionStatusUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeStatusesUpdate)) + case humanauthdomain.PermissionStatusDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeStatusesDelete)) case humanauthdomain.PermissionTicketRead: mapped = append(mapped, string(agentplatformdomain.ScopeTicketsList)) case humanauthdomain.PermissionTicketCreate: mapped = append(mapped, string(agentplatformdomain.ScopeTicketsCreate)) case humanauthdomain.PermissionTicketUpdate: mapped = append(mapped, string(agentplatformdomain.ScopeTicketsUpdate)) - case humanauthdomain.PermissionWorkflowRead, humanauthdomain.PermissionHarnessRead: - mapped = append(mapped, string(agentplatformdomain.ScopeWorkflowsRead)) + case humanauthdomain.PermissionWorkflowRead: + mapped = append(mapped, + string(agentplatformdomain.ScopeWorkflowsList), + string(agentplatformdomain.ScopeWorkflowsRead), + ) + case humanauthdomain.PermissionWorkflowCreate: + mapped = append(mapped, string(agentplatformdomain.ScopeWorkflowsCreate)) + case humanauthdomain.PermissionWorkflowUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeWorkflowsUpdate)) + case humanauthdomain.PermissionWorkflowDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeWorkflowsDelete)) + case humanauthdomain.PermissionHarnessRead: + mapped = append(mapped, + string(agentplatformdomain.ScopeWorkflowsHarnessHistoryRead), + string(agentplatformdomain.ScopeWorkflowsHarnessRead), + string(agentplatformdomain.ScopeWorkflowsHarnessVariablesRead), + ) + case humanauthdomain.PermissionHarnessUpdate: + mapped = append(mapped, + string(agentplatformdomain.ScopeWorkflowsHarnessUpdate), + string(agentplatformdomain.ScopeWorkflowsHarnessValidate), + ) + case humanauthdomain.PermissionSkillRead: + mapped = append(mapped, + string(agentplatformdomain.ScopeSkillsList), + string(agentplatformdomain.ScopeSkillsRead), + ) + case humanauthdomain.PermissionSkillCreate: + mapped = append(mapped, + string(agentplatformdomain.ScopeSkillsCreate), + string(agentplatformdomain.ScopeSkillsImport), + ) + case humanauthdomain.PermissionSkillUpdate: + mapped = append(mapped, + string(agentplatformdomain.ScopeSkillsRefresh), + string(agentplatformdomain.ScopeSkillsUpdate), + ) + case humanauthdomain.PermissionSkillDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeSkillsDelete)) + case humanauthdomain.PermissionAgentRead: + mapped = append(mapped, string(agentplatformdomain.ScopeAgentsRead)) + case humanauthdomain.PermissionAgentCreate: + mapped = append(mapped, string(agentplatformdomain.ScopeAgentsCreate)) + case humanauthdomain.PermissionAgentUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeAgentsUpdate)) + case humanauthdomain.PermissionAgentDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeAgentsDelete)) + case humanauthdomain.PermissionAgentControl: + mapped = append(mapped, + string(agentplatformdomain.ScopeAgentsInterrupt), + string(agentplatformdomain.ScopeAgentsPause), + string(agentplatformdomain.ScopeAgentsResume), + ) + case humanauthdomain.PermissionJobRead: + mapped = append(mapped, string(agentplatformdomain.ScopeScheduledJobsList)) + case humanauthdomain.PermissionJobCreate: + mapped = append(mapped, string(agentplatformdomain.ScopeScheduledJobsCreate)) + case humanauthdomain.PermissionJobUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeScheduledJobsUpdate)) + case humanauthdomain.PermissionJobDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeScheduledJobsDelete)) + case humanauthdomain.PermissionJobTrigger: + mapped = append(mapped, string(agentplatformdomain.ScopeScheduledJobsTrigger)) + case humanauthdomain.PermissionNotificationRead: + mapped = append(mapped, string(agentplatformdomain.ScopeNotificationRulesList)) + case humanauthdomain.PermissionNotificationCreate: + mapped = append(mapped, string(agentplatformdomain.ScopeNotificationRulesCreate)) + case humanauthdomain.PermissionNotificationUpdate: + mapped = append(mapped, string(agentplatformdomain.ScopeNotificationRulesUpdate)) + case humanauthdomain.PermissionNotificationDelete: + mapped = append(mapped, string(agentplatformdomain.ScopeNotificationRulesDelete)) } } return uniqueStrings(mapped) diff --git a/internal/service/userapikey/service_test.go b/internal/service/userapikey/service_test.go new file mode 100644 index 000000000..5c00c3605 --- /dev/null +++ b/internal/service/userapikey/service_test.go @@ -0,0 +1,108 @@ +package userapikey + +import ( + "slices" + "testing" + + agentplatformdomain "github.com/BetterAndBetterII/openase/internal/domain/agentplatform" + humanauthdomain "github.com/BetterAndBetterII/openase/internal/domain/humanauth" +) + +func TestScopesForPermissionsCoversExpandedProjectAutomationScopes(t *testing.T) { + permissions := []humanauthdomain.PermissionKey{ + humanauthdomain.PermissionProjectRead, + humanauthdomain.PermissionProjectUpdate, + humanauthdomain.PermissionProjectUpdateRead, + humanauthdomain.PermissionProjectUpdateCreate, + humanauthdomain.PermissionRepoRead, + humanauthdomain.PermissionRepoCreate, + humanauthdomain.PermissionRepoUpdate, + humanauthdomain.PermissionRepoDelete, + humanauthdomain.PermissionStatusRead, + humanauthdomain.PermissionStatusCreate, + humanauthdomain.PermissionStatusUpdate, + humanauthdomain.PermissionStatusDelete, + humanauthdomain.PermissionTicketRead, + humanauthdomain.PermissionTicketCreate, + humanauthdomain.PermissionTicketUpdate, + humanauthdomain.PermissionWorkflowRead, + humanauthdomain.PermissionWorkflowCreate, + humanauthdomain.PermissionWorkflowUpdate, + humanauthdomain.PermissionWorkflowDelete, + humanauthdomain.PermissionHarnessRead, + humanauthdomain.PermissionHarnessUpdate, + humanauthdomain.PermissionSkillRead, + humanauthdomain.PermissionSkillCreate, + humanauthdomain.PermissionSkillUpdate, + humanauthdomain.PermissionSkillDelete, + humanauthdomain.PermissionAgentRead, + humanauthdomain.PermissionAgentCreate, + humanauthdomain.PermissionAgentUpdate, + humanauthdomain.PermissionAgentDelete, + humanauthdomain.PermissionAgentControl, + humanauthdomain.PermissionJobRead, + humanauthdomain.PermissionJobCreate, + humanauthdomain.PermissionJobUpdate, + humanauthdomain.PermissionJobDelete, + humanauthdomain.PermissionJobTrigger, + humanauthdomain.PermissionNotificationRead, + humanauthdomain.PermissionNotificationCreate, + humanauthdomain.PermissionNotificationUpdate, + humanauthdomain.PermissionNotificationDelete, + } + + got := scopesForPermissions(permissions) + want := []string{ + string(agentplatformdomain.ScopeActivityRead), + string(agentplatformdomain.ScopeAgentsCreate), + string(agentplatformdomain.ScopeAgentsDelete), + string(agentplatformdomain.ScopeAgentsInterrupt), + string(agentplatformdomain.ScopeAgentsPause), + string(agentplatformdomain.ScopeAgentsRead), + string(agentplatformdomain.ScopeAgentsResume), + string(agentplatformdomain.ScopeAgentsUpdate), + string(agentplatformdomain.ScopeNotificationRulesCreate), + string(agentplatformdomain.ScopeNotificationRulesDelete), + string(agentplatformdomain.ScopeNotificationRulesList), + string(agentplatformdomain.ScopeNotificationRulesUpdate), + string(agentplatformdomain.ScopeProjectUpdatesRead), + string(agentplatformdomain.ScopeProjectUpdatesWrite), + string(agentplatformdomain.ScopeProjectsUpdate), + string(agentplatformdomain.ScopeReposCreate), + string(agentplatformdomain.ScopeReposDelete), + string(agentplatformdomain.ScopeReposRead), + string(agentplatformdomain.ScopeReposUpdate), + string(agentplatformdomain.ScopeScheduledJobsCreate), + string(agentplatformdomain.ScopeScheduledJobsDelete), + string(agentplatformdomain.ScopeScheduledJobsList), + string(agentplatformdomain.ScopeScheduledJobsTrigger), + string(agentplatformdomain.ScopeScheduledJobsUpdate), + string(agentplatformdomain.ScopeSkillsCreate), + string(agentplatformdomain.ScopeSkillsDelete), + string(agentplatformdomain.ScopeSkillsImport), + string(agentplatformdomain.ScopeSkillsList), + string(agentplatformdomain.ScopeSkillsRead), + string(agentplatformdomain.ScopeSkillsRefresh), + string(agentplatformdomain.ScopeSkillsUpdate), + string(agentplatformdomain.ScopeStatusesCreate), + string(agentplatformdomain.ScopeStatusesDelete), + string(agentplatformdomain.ScopeStatusesList), + string(agentplatformdomain.ScopeStatusesUpdate), + string(agentplatformdomain.ScopeTicketsCreate), + string(agentplatformdomain.ScopeTicketsList), + string(agentplatformdomain.ScopeTicketsUpdate), + string(agentplatformdomain.ScopeWorkflowsCreate), + string(agentplatformdomain.ScopeWorkflowsDelete), + string(agentplatformdomain.ScopeWorkflowsHarnessHistoryRead), + string(agentplatformdomain.ScopeWorkflowsHarnessRead), + string(agentplatformdomain.ScopeWorkflowsHarnessUpdate), + string(agentplatformdomain.ScopeWorkflowsHarnessValidate), + string(agentplatformdomain.ScopeWorkflowsHarnessVariablesRead), + string(agentplatformdomain.ScopeWorkflowsList), + string(agentplatformdomain.ScopeWorkflowsRead), + string(agentplatformdomain.ScopeWorkflowsUpdate), + } + if !slices.Equal(got, want) { + t.Fatalf("scopesForPermissions() = %#v, want %#v", got, want) + } +} diff --git a/web/src/lib/features/settings/components/security-settings-disabled-auth.test.ts b/web/src/lib/features/settings/components/security-settings-disabled-auth.test.ts index 6c00c5015..5a566938f 100644 --- a/web/src/lib/features/settings/components/security-settings-disabled-auth.test.ts +++ b/web/src/lib/features/settings/components/security-settings-disabled-auth.test.ts @@ -1,34 +1,62 @@ import { cleanup, render } from '@testing-library/svelte' import { afterEach, describe, expect, it, vi } from 'vitest' +import { authStore } from '$lib/stores/auth.svelte' import { appStore } from '$lib/stores/app.svelte' import SecuritySettings from './security-settings.svelte' import { currentOrg, currentProject, disabledSecurity } from './security-settings.test-helpers' -const { getSecuritySettings } = vi.hoisted(() => ({ +const { + getSecuritySettings, + listProjectScopedSecrets, + listProjectUserAPIKeys, + listScopedSecretBindings, + listScopedSecrets, + listTickets, + listWorkflows, +} = vi.hoisted(() => ({ getSecuritySettings: vi.fn(), + listProjectScopedSecrets: vi.fn(), + listProjectUserAPIKeys: vi.fn(), + listScopedSecretBindings: vi.fn(), + listScopedSecrets: vi.fn(), + listTickets: vi.fn(), + listWorkflows: vi.fn(), })) vi.mock('$lib/api/openase', () => ({ getSecuritySettings, + listProjectScopedSecrets, + listProjectUserAPIKeys, + listScopedSecretBindings, + listScopedSecrets, + listTickets, + listWorkflows, })) describe('Security settings disabled auth', () => { afterEach(() => { cleanup() + authStore.clear() appStore.currentOrg = null appStore.currentProject = null vi.clearAllMocks() }) it('does not render the OIDC configuration form in disabled auth mode', async () => { + authStore.hydrate({ authenticated: false, loginRequired: false }) appStore.currentOrg = currentOrg() appStore.currentProject = currentProject() getSecuritySettings.mockResolvedValue({ security: disabledSecurity() }) + listProjectScopedSecrets.mockResolvedValue({ secrets: [] }) + listScopedSecrets.mockResolvedValue({ secrets: [] }) + listScopedSecretBindings.mockResolvedValue({ bindings: [] }) + listTickets.mockResolvedValue({ tickets: [] }) + listWorkflows.mockResolvedValue({ workflows: [] }) + listProjectUserAPIKeys.mockResolvedValue({ api_keys: [] }) - const { findByText, queryByLabelText } = render(SecuritySettings) + const { queryByLabelText } = render(SecuritySettings) - expect(await findByText('GitHub outbound credentials')).toBeTruthy() expect(queryByLabelText('Issuer URL')).toBeNull() }) }) diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte index b49001047..a4b70256b 100644 --- a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte @@ -12,6 +12,7 @@ listProjectUserAPIKeys, rotateProjectUserAPIKey, } from '$lib/api/openase' + import { authStore } from '$lib/stores/auth.svelte' import { ScopeGroupPicker } from '$lib/features/workflows' import { appStore } from '$lib/stores/app.svelte' import { toastStore } from '$lib/stores/toast.svelte' @@ -42,12 +43,26 @@ let selectedScopes = $state([]) let mutationKey = $state('') + const projectId = $derived(security.project_id || appStore.currentProject?.id || '') const scopeGroups = $derived(security.user_api_keys.allowed_scope_groups ?? []) + const requiresHumanSession = $derived(!authStore.authenticated) + const humanSessionMessage = $derived( + authStore.requiresAuthorization + ? i18nStore.t('settings.security.userApiKeys.messages.signInRequired') + : i18nStore.t('settings.security.userApiKeys.messages.humanSessionRequired'), + ) $effect(() => { - const projectId = appStore.currentProject?.id if (!projectId) { items = [] + error = '' + loading = false + return + } + if (requiresHumanSession) { + items = [] + error = '' + loading = false return } let cancelled = false @@ -82,7 +97,6 @@ } async function handleCreate() { - const projectId = appStore.currentProject?.id if (!projectId) return mutationKey = 'create' try { @@ -110,7 +124,6 @@ } async function handleRotate(item: ProjectUserAPIKey) { - const projectId = appStore.currentProject?.id if (!projectId) return mutationKey = `rotate:${item.id}` try { @@ -126,7 +139,6 @@ } async function handleDisable(item: ProjectUserAPIKey) { - const projectId = appStore.currentProject?.id if (!projectId) return mutationKey = `disable:${item.id}` try { @@ -144,7 +156,6 @@ } async function handleDelete(item: ProjectUserAPIKey) { - const projectId = appStore.currentProject?.id if (!projectId) return mutationKey = `delete:${item.id}` try { @@ -179,7 +190,7 @@ {#snippet child({ props })} - {/snippet} @@ -251,6 +262,10 @@
{i18nStore.t('settings.security.userApiKeys.messages.loading')}
+ {:else if requiresHumanSession} +
+ {humanSessionMessage} +
{:else if error}
{error}
{:else if items.length === 0} diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts b/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts index b169a0943..83306f1f4 100644 --- a/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.test.ts @@ -1,6 +1,7 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/svelte' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import { authStore } from '$lib/stores/auth.svelte' import { appStore } from '$lib/stores/app.svelte' import { configuredSecurity, currentOrg, currentProject } from './security-settings.test-helpers' import SecuritySettingsUserAPIKeys from './security-settings-user-api-keys.svelte' @@ -39,12 +40,14 @@ describe('SecuritySettingsUserAPIKeys', () => { afterEach(() => { cleanup() + authStore.clear() appStore.currentOrg = null appStore.currentProject = null vi.clearAllMocks() }) it('shows plaintext API key once after creating a project user API key', async () => { + authStore.hydrate({ authenticated: true }) appStore.currentOrg = currentOrg() appStore.currentProject = currentProject() listProjectUserAPIKeys.mockResolvedValue({ api_keys: [] }) @@ -90,4 +93,36 @@ describe('SecuritySettingsUserAPIKeys', () => { expect(queryByText('ase_pat_plain_text_token')).toBeNull() }) }) + + it('uses the loaded security project id for API calls', async () => { + authStore.hydrate({ authenticated: true }) + appStore.currentOrg = currentOrg() + appStore.currentProject = { ...currentProject(), id: 'atlas' } + listProjectUserAPIKeys.mockResolvedValue({ api_keys: [] }) + + render(SecuritySettingsUserAPIKeys, { security: configuredSecurity() }) + + await waitFor(() => { + expect(listProjectUserAPIKeys).toHaveBeenCalledWith(configuredSecurity().project_id) + }) + }) + + it('shows a sign-in message instead of calling the API without a human session', async () => { + authStore.hydrate({ + authenticated: false, + loginRequired: true, + authCapabilities: { availableAuthMethods: ['oidc'], currentAuthMethod: 'oidc' }, + }) + appStore.currentOrg = currentOrg() + appStore.currentProject = currentProject() + + const { findByText } = render(SecuritySettingsUserAPIKeys, { security: configuredSecurity() }) + + expect( + await findByText( + 'Sign in to list, create, rotate, disable, or delete user API keys for this project.', + ), + ).toBeTruthy() + expect(listProjectUserAPIKeys).not.toHaveBeenCalled() + }) }) diff --git a/web/src/lib/features/settings/components/security-settings.test.ts b/web/src/lib/features/settings/components/security-settings.test.ts index 33aba5578..63b6e0e6c 100644 --- a/web/src/lib/features/settings/components/security-settings.test.ts +++ b/web/src/lib/features/settings/components/security-settings.test.ts @@ -1,7 +1,8 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/svelte' -import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { ScopedSecretRecord } from '$lib/api/contracts' +import { authStore } from '$lib/stores/auth.svelte' import { appStore } from '$lib/stores/app.svelte' import SecuritySettings from './security-settings.svelte' import { @@ -98,8 +99,13 @@ describe('Security settings', () => { HTMLElement.prototype.releasePointerCapture ??= vi.fn() }) + beforeEach(() => { + authStore.hydrate({ authenticated: true }) + }) + afterEach(() => { cleanup() + authStore.clear() appStore.currentOrg = null appStore.currentProject = null vi.clearAllMocks() diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index 882863cfe..be4d96e32 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -2843,9 +2843,11 @@ "settings.security.userApiKeys.labels.expiresAt": "Expires at", "settings.security.userApiKeys.labels.name": "Name", "settings.security.userApiKeys.messages.empty": "No project API keys yet.", + "settings.security.userApiKeys.messages.humanSessionRequired": "User API keys require a human session. Create a local bootstrap session or enable sign-in before managing keys.", "settings.security.userApiKeys.messages.loading": "Loading API keys...", "settings.security.userApiKeys.messages.metadata": "Created {created} · Last used {lastUsed} · Expires {expires}", "settings.security.userApiKeys.messages.noAllowedScopes": "No mintable scopes are available for your current project permissions.", + "settings.security.userApiKeys.messages.signInRequired": "Sign in to list, create, rotate, disable, or delete user API keys for this project.", "settings.security.userApiKeys.notifications.copied": "Token copied", "settings.security.userApiKeys.notifications.created": "API key created", "settings.security.userApiKeys.notifications.deleted": "API key deleted", diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index c418a3766..f1f714a63 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -2843,9 +2843,11 @@ "settings.security.userApiKeys.labels.expiresAt": "过期时间", "settings.security.userApiKeys.labels.name": "名称", "settings.security.userApiKeys.messages.empty": "当前还没有项目 API Key。", + "settings.security.userApiKeys.messages.humanSessionRequired": "用户 API Key 需要 human session。请先创建本地 bootstrap 会话,或启用登录后再管理这些 Key。", "settings.security.userApiKeys.messages.loading": "正在加载 API Key...", "settings.security.userApiKeys.messages.metadata": "创建于 {created} · 最近使用 {lastUsed} · 过期时间 {expires}", "settings.security.userApiKeys.messages.noAllowedScopes": "基于你当前的项目权限,没有可签发的 scope。", + "settings.security.userApiKeys.messages.signInRequired": "请先登录,才能查看、创建、轮转、禁用或删除这个项目的用户 API Key。", "settings.security.userApiKeys.notifications.copied": "令牌已复制", "settings.security.userApiKeys.notifications.created": "API Key 已创建", "settings.security.userApiKeys.notifications.deleted": "API Key 已删除", From 3bf4cab7ec12c8af2aed33efa6ee0eb62d2ce7c1 Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Fri, 17 Apr 2026 05:55:31 +0100 Subject: [PATCH 13/14] refactor(user-api-keys): split reveal dialog --- ...settings-user-api-key-reveal-dialog.svelte | 44 +++++++++++++++++++ .../security-settings-user-api-keys.svelte | 42 +++++------------- 2 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 web/src/lib/features/settings/components/security-settings-user-api-key-reveal-dialog.svelte diff --git a/web/src/lib/features/settings/components/security-settings-user-api-key-reveal-dialog.svelte b/web/src/lib/features/settings/components/security-settings-user-api-key-reveal-dialog.svelte new file mode 100644 index 000000000..786d993ba --- /dev/null +++ b/web/src/lib/features/settings/components/security-settings-user-api-key-reveal-dialog.svelte @@ -0,0 +1,44 @@ + + + + + + {i18nStore.t('settings.security.userApiKeys.reveal.title')} + + {i18nStore.t('settings.security.userApiKeys.reveal.description', { name })} + + + +
{token}
+

+ {i18nStore.t('settings.security.userApiKeys.reveal.warning')} +

+
+ + + + {#snippet child({ props })} + + {/snippet} + + +
+
diff --git a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte index a4b70256b..f884fa5ec 100644 --- a/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte +++ b/web/src/lib/features/settings/components/security-settings-user-api-keys.svelte @@ -22,6 +22,7 @@ import { Input } from '$ui/input' import { Label } from '$ui/label' import { i18nStore } from '$lib/i18n/store.svelte' + import SecuritySettingsUserAPIKeyRevealDialog from './security-settings-user-api-key-reveal-dialog.svelte' import { formatUserAPIKeyTime, toRFC3339Local } from './security-settings-user-api-keys.model' type Security = SecuritySettingsResponse['security'] @@ -89,7 +90,6 @@ ? caughtError.detail : i18nStore.t('settings.security.userApiKeys.errors.requestFailed') } - function resetDraft() { name = '' expiresAtLocal = '' @@ -198,9 +198,9 @@ {i18nStore.t('settings.security.userApiKeys.dialog.title')} - - {i18nStore.t('settings.security.userApiKeys.dialog.description')} - + {i18nStore.t('settings.security.userApiKeys.dialog.description')}
@@ -329,31 +329,9 @@ {/if}
- - - - {i18nStore.t('settings.security.userApiKeys.reveal.title')} - - {i18nStore.t('settings.security.userApiKeys.reveal.description', { - name: currentTokenName, - })} - - - -
{currentToken}
-

- {i18nStore.t('settings.security.userApiKeys.reveal.warning')} -

-
- - - - {#snippet child({ props })} - - {/snippet} - - -
-
+ From 3f11dc71f56a27a3166ce5a5cf9ee5797a8793fa Mon Sep 17 00:00:00 2001 From: betterandbetterii Date: Fri, 17 Apr 2026 06:03:11 +0100 Subject: [PATCH 14/14] fix(tickets): disable card drag on mobile --- web/src/lib/features/board/components/ticket-card.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/lib/features/board/components/ticket-card.svelte b/web/src/lib/features/board/components/ticket-card.svelte index 36acb9c26..04510d8af 100644 --- a/web/src/lib/features/board/components/ticket-card.svelte +++ b/web/src/lib/features/board/components/ticket-card.svelte @@ -2,6 +2,7 @@ import type { TranslationKey } from '$lib/i18n' import { cn, formatRelativeTime, truncate } from '$lib/utils' import { i18nStore } from '$lib/i18n/store.svelte' + import { viewport } from '$lib/stores/viewport.svelte' import { Badge } from '$ui/badge' import { RotateCcw, @@ -81,6 +82,7 @@ let suppressClickUntil = 0 let contextMenuOpen = $state(false) + const draggable = $derived(viewport.isDesktop && !isPendingMove && !contextMenuOpen) function handleClick() { if (Date.now() < suppressClickUntil || isDragging || contextMenuOpen) { @@ -162,7 +164,7 @@ >