From b042101cb075e4cbc0a28a9a34944784d9d317b3 Mon Sep 17 00:00:00 2001 From: jjchen01 Date: Thu, 28 Aug 2025 18:27:02 +0800 Subject: [PATCH 1/2] openai platform projects --- .../app/domain/project/project_service.go | 2 +- .../http/responses/openai/response.go | 3 +- .../interfaces/http/routes/routes_provider.go | 2 + .../routes/v1/organization/admin_api_keys.go | 2 +- .../v1/organization/organization_route.go | 6 +- .../v1/organization/projects/project_route.go | 467 ++++++++++++++++++ .../application/app/utils/ptr/pointer.go | 6 + .../application/cmd/server/wire_gen.go | 4 +- apps/jan-api-gateway/application/docs/docs.go | 371 ++++++++++++++ .../application/docs/swagger.json | 371 ++++++++++++++ .../application/docs/swagger.yaml | 247 +++++++++ 11 files changed, 1476 insertions(+), 5 deletions(-) create mode 100644 apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/projects/project_route.go diff --git a/apps/jan-api-gateway/application/app/domain/project/project_service.go b/apps/jan-api-gateway/application/app/domain/project/project_service.go index c6392914..b52fabc3 100644 --- a/apps/jan-api-gateway/application/app/domain/project/project_service.go +++ b/apps/jan-api-gateway/application/app/domain/project/project_service.go @@ -80,7 +80,7 @@ func (s *ProjectService) FindProjectByPublicID(ctx context.Context, publicID str } // FindProjects retrieves a list of projects based on a filter and pagination. -func (s *ProjectService) FindProjects(ctx context.Context, filter ProjectFilter, pagination *query.Pagination) ([]*Project, error) { +func (s *ProjectService) Find(ctx context.Context, filter ProjectFilter, pagination *query.Pagination) ([]*Project, error) { return s.repo.FindByFilter(ctx, filter, pagination) } diff --git a/apps/jan-api-gateway/application/app/interfaces/http/responses/openai/response.go b/apps/jan-api-gateway/application/app/interfaces/http/responses/openai/response.go index a74b6e99..f0e76352 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/responses/openai/response.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/responses/openai/response.go @@ -3,7 +3,8 @@ package openai type ObjectKey string const ( - OrganizationAdminApiKey ObjectKey = "organization.admin_api_key" + ObjectKeyAdminApiKey ObjectKey = "organization.admin_api_key" + ObjectKeyProject ObjectKey = "organization.project" ) type OwnerType string diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go index 43cd9d7e..ffb78beb 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/routes_provider.go @@ -13,6 +13,7 @@ import ( "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/mcp" mcp_impl "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/mcp/mcp_impl" "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/organization" + "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/organization/projects" ) var RouteProvider = wire.NewSet( @@ -23,6 +24,7 @@ var RouteProvider = wire.NewSet( janV1Auth.NewAuthRoute, janV1.NewV1Route, jan.NewJanRoute, + projects.NewProjectsRoute, organization.NewAdminApiKeyAPI, organization.NewOrganizationRoute, mcp_impl.NewSerperMCP, diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/admin_api_keys.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/admin_api_keys.go index 1c103851..bdaa4540 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/admin_api_keys.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/admin_api_keys.go @@ -366,7 +366,7 @@ func domainToOrganizationAdminAPIKeyResponse(entity *apikey.ApiKey) Organization lastUsedAt = ptr.ToInt64(entity.LastUsedAt.Unix()) } return OrganizationAdminAPIKeyResponse{ - Object: string(openai.OrganizationAdminApiKey), + Object: string(openai.ObjectKeyAdminApiKey), ID: entity.PublicID, Name: entity.Description, RedactedValue: entity.PlaintextHint, diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/organization_route.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/organization_route.go index a97296e5..2f492548 100644 --- a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/organization_route.go +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/organization_route.go @@ -2,19 +2,23 @@ package organization import ( "github.com/gin-gonic/gin" + "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/organization/projects" ) type OrganizationRoute struct { adminApiKeyAPI *AdminApiKeyAPI + projectsRoute *projects.ProjectsRoute } -func NewOrganizationRoute(adminApiKeyAPI *AdminApiKeyAPI) *OrganizationRoute { +func NewOrganizationRoute(adminApiKeyAPI *AdminApiKeyAPI, projectsRoute *projects.ProjectsRoute) *OrganizationRoute { return &OrganizationRoute{ adminApiKeyAPI, + projectsRoute, } } func (organizationRoute *OrganizationRoute) RegisterRouter(router gin.IRouter) { organizationRouter := router.Group("/organization") organizationRoute.adminApiKeyAPI.RegisterRouter(organizationRouter) + organizationRoute.projectsRoute.RegisterRouter(organizationRouter) } diff --git a/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/projects/project_route.go b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/projects/project_route.go new file mode 100644 index 00000000..122ad5f6 --- /dev/null +++ b/apps/jan-api-gateway/application/app/interfaces/http/routes/v1/organization/projects/project_route.go @@ -0,0 +1,467 @@ +package projects + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "menlo.ai/jan-api-gateway/app/domain/apikey" + "menlo.ai/jan-api-gateway/app/domain/project" + "menlo.ai/jan-api-gateway/app/domain/query" + "menlo.ai/jan-api-gateway/app/interfaces/http/requests" + "menlo.ai/jan-api-gateway/app/interfaces/http/responses" + "menlo.ai/jan-api-gateway/app/interfaces/http/responses/openai" + "menlo.ai/jan-api-gateway/app/utils/functional" + "menlo.ai/jan-api-gateway/app/utils/ptr" +) + +type ProjectsRoute struct { + projectService *project.ProjectService + apiKeyService *apikey.ApiKeyService +} + +func NewProjectsRoute(projectService *project.ProjectService, apiKeyService *apikey.ApiKeyService) *ProjectsRoute { + return &ProjectsRoute{ + projectService, + apiKeyService, + } +} + +func (projectsRoute *ProjectsRoute) RegisterRouter(router gin.IRouter) { + projectsRouter := router.Group("/projects") + projectsRouter.GET("", projectsRoute.GetProjects) + projectsRouter.POST("", projectsRoute.CreateProject) + projectsRouter.GET("/:project_id", projectsRoute.GetProject) + projectsRouter.POST("/:project_id", projectsRoute.UpdateProject) + projectsRouter.POST("/:project_id/archive", projectsRoute.ArchiveProject) +} + +// GetProjects godoc +// @Summary List Projects +// @Description Retrieves a paginated list of all projects for the authenticated organization. +// @Tags Projects +// @Security BearerAuth +// @Param Authorization header string true "Bearer token" default("Bearer ") +// @Param limit query int false "The maximum number of items to return" default(20) +// @Param after query string false "A cursor for use in pagination. The ID of the last object from the previous page" +// @Param include_archived query string false "Whether to include archived projects." +// @Success 200 {object} ProjectListResponse "Successfully retrieved the list of projects" +// @Failure 401 {object} responses.ErrorResponse "Unauthorized - invalid or missing API key" +// @Failure 500 {object} responses.ErrorResponse "Internal Server Error" +// @Router /v1/organization/projects [get] +func (api *ProjectsRoute) GetProjects(reqCtx *gin.Context) { + projectService := api.projectService + includeArchivedStr := reqCtx.DefaultQuery("include_archived", "false") + includeArchived, err := strconv.ParseBool(includeArchivedStr) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "65e69a2c-5ce0-4a9c-bb61-ee5cc494f948", + Error: "invalid or missing query parameter", + }) + return + } + + ctx := reqCtx.Request.Context() + adminKey, err := api.validateAdminKey(reqCtx) + if err != nil { + return + } + + pagination, err := query.GetPaginationFromQuery(reqCtx) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "4434f5ed-89f4-4a62-9fef-8ca53336dcda", + Error: "invalid or missing query parameter", + }) + return + } + + afterStr := reqCtx.Query("after") + if afterStr != "" { + entity, err := projectService.Find(ctx, project.ProjectFilter{ + PublicID: &afterStr, + }, &query.Pagination{ + Limit: ptr.ToInt(1), + }) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "20f37a43-1c2e-4efe-9f5b-c1d0b1ccdd58", + Error: err.Error(), + }) + return + } + if len(entity) == 0 { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "a8a65f59-7b92-4b09-87eb-993eb19188e6", + Error: "failed to retrieve projects", + }) + return + } + pagination.After = &entity[0].ID + } + + projectFilter := project.ProjectFilter{ + OrganizationID: adminKey.OrganizationID, + } + if !includeArchived { + projectFilter.Archived = ptr.ToBool(false) + } + projects, err := projectService.Find(ctx, projectFilter, pagination) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "29d3d0b0-e587-4f20-9adb-1ab9aa666b38", + Error: "failed to retrieve projects", + }) + return + } + + var firstId *string + var lastId *string + hasMore := false + if len(projects) > 0 { + firstId = &projects[0].PublicID + lastId = &projects[len(projects)-1].PublicID + moreRecords, err := projectService.Find(ctx, projectFilter, &query.Pagination{ + Order: pagination.Order, + Limit: ptr.ToInt(1), + After: &projects[len(projects)-1].ID, + }) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "1be3dfc2-f2ce-4b0e-a385-1cc6f4324398", + Error: "failed to retrieve API keys", + }) + return + } + if len(moreRecords) != 0 { + hasMore = true + } + } + + result := functional.Map(projects, func(project *project.Project) ProjectResponse { + return domainToProjectResponse(project) + }) + + response := ProjectListResponse{ + Object: "list", + Data: result, + HasMore: hasMore, + FirstID: firstId, + LastID: lastId, + } + reqCtx.JSON(http.StatusOK, response) +} + +// CreateProject godoc +// @Summary Create Project +// @Description Creates a new project for an organization. +// @Tags Projects +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param Authorization header string true "Bearer token" default("Bearer ") +// @Param body body CreateProjectRequest true "Project creation request" +// @Success 200 {object} ProjectResponse "Successfully created project" +// @Failure 400 {object} responses.ErrorResponse "Bad request - invalid payload" +// @Failure 401 {object} responses.ErrorResponse "Unauthorized - invalid or missing API key" +// @Failure 500 {object} responses.ErrorResponse "Internal Server Error" +// @Router /v1/organization/projects [post] +func (api *ProjectsRoute) CreateProject(reqCtx *gin.Context) { + projectService := api.projectService + ctx := reqCtx.Request.Context() + + var requestPayload CreateProjectRequest + if err := reqCtx.ShouldBindJSON(&requestPayload); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "db8142f8-dc78-4581-a238-6e32288a54ec", + Error: err.Error(), + }) + return + } + + adminKey, err := api.validateAdminKey(reqCtx) + if err != nil { + return + } + + projectEntity, err := projectService.CreateProjectWithPublicID(ctx, &project.Project{ + Name: requestPayload.Name, + OrganizationID: *adminKey.OrganizationID, + Status: string(project.ProjectStatusActive), + }) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "e00e6ab3-1b43-490e-90df-aae030697f74", + Error: err.Error(), + }) + return + } + + response := domainToProjectResponse(projectEntity) + reqCtx.JSON(http.StatusOK, response) +} + +// GetProject godoc +// @Summary Get Project +// @Description Retrieves a specific project by its ID. +// @Tags Projects +// @Security BearerAuth +// @Param Authorization header string true "Bearer token" default("Bearer ") +// @Param project_id path string true "ID of the project" +// @Success 200 {object} ProjectResponse "Successfully retrieved the project" +// @Failure 401 {object} responses.ErrorResponse "Unauthorized - invalid or missing API key" +// @Failure 404 {object} responses.ErrorResponse "Not Found - project with the given ID does not exist or does not belong to the organization" +// @Router /v1/organization/projects/{project_id} [get] +func (api *ProjectsRoute) GetProject(reqCtx *gin.Context) { + projectService := api.projectService + ctx := reqCtx.Request.Context() + + adminKey, err := api.validateAdminKey(reqCtx) + if err != nil { + return + } + + projectID := reqCtx.Param("project_id") + if projectID == "" { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "e8100503-698f-4ee6-8f5c-3274f5476e67", + Error: "invalid or missing project ID", + }) + return + } + + entity, err := projectService.FindProjectByPublicID(ctx, projectID) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "42ad3a04-6c17-40db-a10f-640be569c93f", + Error: "project not found", + }) + return + } + + if entity.OrganizationID != *adminKey.OrganizationID { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "752f93d3-21f1-45a3-ba13-0157d069aca2", + Error: "project not found in organization", + }) + return + } + + reqCtx.JSON(http.StatusOK, domainToProjectResponse(entity)) +} + +// UpdateProject godoc +// @Summary Update Project +// @Description Updates a specific project by its ID. +// @Tags Projects +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param Authorization header string true "Bearer token" default("Bearer ") +// @Param project_id path string true "ID of the project to update" +// @Param body body UpdateProjectRequest true "Project update request" +// @Success 200 {object} ProjectResponse "Successfully updated the project" +// @Failure 400 {object} responses.ErrorResponse "Bad request - invalid payload" +// @Failure 401 {object} responses.ErrorResponse "Unauthorized - invalid or missing API key" +// @Failure 404 {object} responses.ErrorResponse "Not Found - project with the given ID does not exist" +// @Router /v1/organization/projects/{project_id} [post] +func (api *ProjectsRoute) UpdateProject(reqCtx *gin.Context) { + projectService := api.projectService + ctx := reqCtx.Request.Context() + + var requestPayload UpdateProjectRequest + if err := reqCtx.ShouldBindJSON(&requestPayload); err != nil { + reqCtx.AbortWithStatusJSON(http.StatusBadRequest, responses.ErrorResponse{ + Code: "b6cb35be-8a53-478d-95d1-5e1f64f35c09", + Error: err.Error(), + }) + return + } + + adminKey, err := api.validateAdminKey(reqCtx) + if err != nil { + return + } + + projectID := reqCtx.Param("project_id") + if projectID == "" { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "a50a180b-75ab-4292-9397-781f66f1502d", + Error: "invalid or missing project ID", + }) + return + } + + entity, err := projectService.FindProjectByPublicID(ctx, projectID) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "4ee156ce-6425-425b-b9fd-d95165456b6c", + Error: "project not found", + }) + return + } + + if entity.OrganizationID != *adminKey.OrganizationID { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "bd69b6c5-2a54-421e-b3b9-b740d4a92f19", + Error: "project not found in organization", + }) + return + } + + // Update the project name if provided + if requestPayload.Name != nil { + entity.Name = *requestPayload.Name + } + + updatedEntity, err := projectService.UpdateProject(ctx, entity) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "c9a103b2-985c-44b7-9ccd-38e914a2c82b", + Error: "failed to update project", + }) + return + } + + reqCtx.JSON(http.StatusOK, domainToProjectResponse(updatedEntity)) +} + +// ArchiveProject godoc +// @Summary Archive Project +// @Description Archives a specific project by its ID, making it inactive. +// @Tags Projects +// @Security BearerAuth +// @Param Authorization header string true "Bearer token" default("Bearer ") +// @Param project_id path string true "ID of the project to archive" +// @Success 200 {object} ProjectResponse "Successfully archived the project" +// @Failure 401 {object} responses.ErrorResponse "Unauthorized - invalid or missing API key" +// @Failure 404 {object} responses.ErrorResponse "Not Found - project with the given ID does not exist" +// @Router /v1/organization/projects/{project_id}/archive [post] +func (api *ProjectsRoute) ArchiveProject(reqCtx *gin.Context) { + projectService := api.projectService + ctx := reqCtx.Request.Context() + + adminKey, err := api.validateAdminKey(reqCtx) + if err != nil { + return + } + + projectID := reqCtx.Param("project_id") + if projectID == "" { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "2ab393c5-708d-42bc-a785-dcbdcf429ad1", + Error: "invalid or missing project ID", + }) + return + } + + entity, err := projectService.FindProjectByPublicID(ctx, projectID) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "26b68f41-0eb0-4fca-8365-613742ef9204", + Error: "project not found", + }) + return + } + + if entity.OrganizationID != *adminKey.OrganizationID { + reqCtx.AbortWithStatusJSON(http.StatusNotFound, responses.ErrorResponse{ + Code: "4b656858-4212-451a-9ab6-23bc09dcc357", + Error: "project not found in organization", + }) + return + } + + // Set archived status + entity.Status = string(project.ProjectStatusArchived) + entity.ArchivedAt = ptr.ToTime(time.Now()) + updatedEntity, err := projectService.UpdateProject(ctx, entity) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusInternalServerError, responses.ErrorResponse{ + Code: "c9a103b2-985c-44b7-9ccd-38e914a2c82b", + Error: "failed to archive project", + }) + return + } + + reqCtx.JSON(http.StatusOK, domainToProjectResponse(updatedEntity)) +} + +// TODO: move to middleware +func (api *ProjectsRoute) validateAdminKey(reqCtx *gin.Context) (*apikey.ApiKey, error) { + apikeyService := api.apiKeyService + ctx := reqCtx.Request.Context() + adminKey, ok := requests.GetTokenFromBearer(reqCtx) + if !ok { + reqCtx.AbortWithStatusJSON(http.StatusUnauthorized, responses.ErrorResponse{ + Code: "704ff768-2681-4ba0-bc6b-600c6f10df25", + Error: "invalid or missing API key", + }) + return nil, fmt.Errorf("invalid token") + } + + // Verify the provided admin API key + adminKeyEntity, err := apikeyService.FindByKey(ctx, adminKey) + if err != nil { + reqCtx.AbortWithStatusJSON(http.StatusUnauthorized, responses.ErrorResponse{ + Code: "af7eb57f-8a57-45c8-8ad7-e60c1c68cb6f", + Error: "invalid or missing API key", + }) + return nil, err + } + + if adminKeyEntity.OwnerType != string(apikey.OwnerTypeAdmin) { + reqCtx.AbortWithStatusJSON(http.StatusUnauthorized, responses.ErrorResponse{ + Code: "2f83ee2f-3054-40de-afd0-82f06d3fb6cb", + Error: "invalid or missing API key", + }) + return nil, fmt.Errorf("invalid or missing API key") + } + return adminKeyEntity, nil +} + +// ProjectResponse defines the response structure for a project. +type ProjectResponse struct { + Object string `json:"object" example:"project" description:"The type of the object, 'project'"` + ID string `json:"id" example:"proj_1234567890" description:"Unique identifier for the project"` + Name string `json:"name" example:"My First Project" description:"The name of the project"` + CreatedAt int64 `json:"created_at" example:"1698765432" description:"Unix timestamp when the project was created"` + ArchivedAt *int64 `json:"archived_at,omitempty" example:"1698765432" description:"Unix timestamp when the project was archived, if applicable"` + Status string `json:"status"` +} + +// CreateProjectRequest defines the request payload for creating a project. +type CreateProjectRequest struct { + Name string `json:"name" binding:"required" example:"New AI Project" description:"The name of the project to be created"` +} + +// UpdateProjectRequest defines the request payload for updating a project. +type UpdateProjectRequest struct { + Name *string `json:"name" example:"Updated AI Project" description:"The new name for the project"` +} + +// ProjectListResponse defines the response structure for a list of projects. +type ProjectListResponse struct { + Object string `json:"object" example:"list" description:"The type of the object, 'list'"` + Data []ProjectResponse `json:"data" description:"Array of projects"` + FirstID *string `json:"first_id,omitempty"` + LastID *string `json:"last_id,omitempty"` + HasMore bool `json:"has_more"` +} + +func domainToProjectResponse(p *project.Project) ProjectResponse { + var archivedAt *int64 + if p.ArchivedAt != nil { + archivedAt = ptr.ToInt64(p.CreatedAt.Unix()) + } + return ProjectResponse{ + Object: string(openai.ObjectKeyProject), + ID: p.PublicID, + Name: p.Name, + CreatedAt: p.CreatedAt.Unix(), + ArchivedAt: archivedAt, + Status: p.Status, + } +} diff --git a/apps/jan-api-gateway/application/app/utils/ptr/pointer.go b/apps/jan-api-gateway/application/app/utils/ptr/pointer.go index 3bf95bbd..ba14e080 100644 --- a/apps/jan-api-gateway/application/app/utils/ptr/pointer.go +++ b/apps/jan-api-gateway/application/app/utils/ptr/pointer.go @@ -1,5 +1,7 @@ package ptr +import "time" + func ToString(s string) *string { return &s } @@ -19,3 +21,7 @@ func ToUint(i uint) *uint { func ToBool(b bool) *bool { return &b } + +func ToTime(b time.Time) *time.Time { + return &b +} diff --git a/apps/jan-api-gateway/application/cmd/server/wire_gen.go b/apps/jan-api-gateway/application/cmd/server/wire_gen.go index 8710e362..ba7e1a97 100644 --- a/apps/jan-api-gateway/application/cmd/server/wire_gen.go +++ b/apps/jan-api-gateway/application/cmd/server/wire_gen.go @@ -30,6 +30,7 @@ import ( "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/mcp" "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/mcp/mcp_impl" organization2 "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/organization" + "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/organization/projects" ) import ( @@ -54,7 +55,8 @@ func CreateApplication() (*Application, error) { userRepository := userrepo.NewUserGormRepository(transactionDatabase) userService := user.NewService(userRepository, organizationService) adminApiKeyAPI := organization2.NewAdminApiKeyAPI(organizationService, apiKeyService, userService) - organizationRoute := organization2.NewOrganizationRoute(adminApiKeyAPI) + projectsRoute := projects.NewProjectsRoute(projectService, apiKeyService) + organizationRoute := organization2.NewOrganizationRoute(adminApiKeyAPI, projectsRoute) completionAPI := chat.NewCompletionAPI(apiKeyService) chatRoute := chat.NewChatRoute(completionAPI) modelAPI := v1.NewModelAPI() diff --git a/apps/jan-api-gateway/application/docs/docs.go b/apps/jan-api-gateway/application/docs/docs.go index 4d0132ad..04ced374 100644 --- a/apps/jan-api-gateway/application/docs/docs.go +++ b/apps/jan-api-gateway/application/docs/docs.go @@ -760,6 +760,304 @@ const docTemplate = `{ } } }, + "/v1/organization/projects": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieves a paginated list of all projects for the authenticated organization.", + "tags": [ + "Projects" + ], + "summary": "List Projects", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "The maximum number of items to return", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "A cursor for use in pagination. The ID of the last object from the previous page", + "name": "after", + "in": "query" + }, + { + "type": "string", + "description": "Whether to include archived projects.", + "name": "include_archived", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successfully retrieved the list of projects", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectListResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new project for an organization.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Projects" + ], + "summary": "Create Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Project creation request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.CreateProjectRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully created project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "400": { + "description": "Bad request - invalid payload", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/organization/projects/{project_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieves a specific project by its ID.", + "tags": [ + "Projects" + ], + "summary": "Get Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the project", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved the project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found - project with the given ID does not exist or does not belong to the organization", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates a specific project by its ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Projects" + ], + "summary": "Update Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the project to update", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Project update request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.UpdateProjectRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated the project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "400": { + "description": "Bad request - invalid payload", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found - project with the given ID does not exist", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/organization/projects/{project_id}/archive": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Archives a specific project by its ID, making it inactive.", + "tags": [ + "Projects" + ], + "summary": "Archive Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the project to archive", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully archived the project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found - project with the given ID does not exist", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, "/v1/version": { "get": { "description": "Returns the current build version of the API server.", @@ -1081,6 +1379,79 @@ const docTemplate = `{ } } }, + "app_interfaces_http_routes_v1_organization_projects.CreateProjectRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "example": "New AI Project" + } + } + }, + "app_interfaces_http_routes_v1_organization_projects.ProjectListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "first_id": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": "string" + }, + "object": { + "type": "string", + "example": "list" + } + } + }, + "app_interfaces_http_routes_v1_organization_projects.ProjectResponse": { + "type": "object", + "properties": { + "archived_at": { + "type": "integer", + "example": 1698765432 + }, + "created_at": { + "type": "integer", + "example": 1698765432 + }, + "id": { + "type": "string", + "example": "proj_1234567890" + }, + "name": { + "type": "string", + "example": "My First Project" + }, + "object": { + "type": "string", + "example": "project" + }, + "status": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_organization_projects.UpdateProjectRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Updated AI Project" + } + } + }, "menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse": { "type": "object", "properties": { diff --git a/apps/jan-api-gateway/application/docs/swagger.json b/apps/jan-api-gateway/application/docs/swagger.json index e9ea5843..b06c07ae 100644 --- a/apps/jan-api-gateway/application/docs/swagger.json +++ b/apps/jan-api-gateway/application/docs/swagger.json @@ -753,6 +753,304 @@ } } }, + "/v1/organization/projects": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieves a paginated list of all projects for the authenticated organization.", + "tags": [ + "Projects" + ], + "summary": "List Projects", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "The maximum number of items to return", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "A cursor for use in pagination. The ID of the last object from the previous page", + "name": "after", + "in": "query" + }, + { + "type": "string", + "description": "Whether to include archived projects.", + "name": "include_archived", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successfully retrieved the list of projects", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectListResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new project for an organization.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Projects" + ], + "summary": "Create Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Project creation request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.CreateProjectRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully created project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "400": { + "description": "Bad request - invalid payload", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/organization/projects/{project_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieves a specific project by its ID.", + "tags": [ + "Projects" + ], + "summary": "Get Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the project", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved the project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found - project with the given ID does not exist or does not belong to the organization", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates a specific project by its ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Projects" + ], + "summary": "Update Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the project to update", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Project update request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.UpdateProjectRequest" + } + } + ], + "responses": { + "200": { + "description": "Successfully updated the project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "400": { + "description": "Bad request - invalid payload", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found - project with the given ID does not exist", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, + "/v1/organization/projects/{project_id}/archive": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Archives a specific project by its ID, making it inactive.", + "tags": [ + "Projects" + ], + "summary": "Archive Project", + "parameters": [ + { + "type": "string", + "default": "\"Bearer \u003capi_key\u003e\"", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the project to archive", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully archived the project", + "schema": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "401": { + "description": "Unauthorized - invalid or missing API key", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + }, + "404": { + "description": "Not Found - project with the given ID does not exist", + "schema": { + "$ref": "#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse" + } + } + } + } + }, "/v1/version": { "get": { "description": "Returns the current build version of the API server.", @@ -1074,6 +1372,79 @@ } } }, + "app_interfaces_http_routes_v1_organization_projects.CreateProjectRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "example": "New AI Project" + } + } + }, + "app_interfaces_http_routes_v1_organization_projects.ProjectListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse" + } + }, + "first_id": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "last_id": { + "type": "string" + }, + "object": { + "type": "string", + "example": "list" + } + } + }, + "app_interfaces_http_routes_v1_organization_projects.ProjectResponse": { + "type": "object", + "properties": { + "archived_at": { + "type": "integer", + "example": 1698765432 + }, + "created_at": { + "type": "integer", + "example": 1698765432 + }, + "id": { + "type": "string", + "example": "proj_1234567890" + }, + "name": { + "type": "string", + "example": "My First Project" + }, + "object": { + "type": "string", + "example": "project" + }, + "status": { + "type": "string" + } + } + }, + "app_interfaces_http_routes_v1_organization_projects.UpdateProjectRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Updated AI Project" + } + } + }, "menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse": { "type": "object", "properties": { diff --git a/apps/jan-api-gateway/application/docs/swagger.yaml b/apps/jan-api-gateway/application/docs/swagger.yaml index 0e9ee35e..64230584 100644 --- a/apps/jan-api-gateway/application/docs/swagger.yaml +++ b/apps/jan-api-gateway/application/docs/swagger.yaml @@ -197,6 +197,56 @@ definitions: example: user type: string type: object + app_interfaces_http_routes_v1_organization_projects.CreateProjectRequest: + properties: + name: + example: New AI Project + type: string + required: + - name + type: object + app_interfaces_http_routes_v1_organization_projects.ProjectListResponse: + properties: + data: + items: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse' + type: array + first_id: + type: string + has_more: + type: boolean + last_id: + type: string + object: + example: list + type: string + type: object + app_interfaces_http_routes_v1_organization_projects.ProjectResponse: + properties: + archived_at: + example: 1698765432 + type: integer + created_at: + example: 1698765432 + type: integer + id: + example: proj_1234567890 + type: string + name: + example: My First Project + type: string + object: + example: project + type: string + status: + type: string + type: object + app_interfaces_http_routes_v1_organization_projects.UpdateProjectRequest: + properties: + name: + example: Updated AI Project + type: string + type: object menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse: properties: code: @@ -1220,6 +1270,203 @@ paths: summary: Get Admin API Key tags: - Admin API Keys + /v1/organization/projects: + get: + description: Retrieves a paginated list of all projects for the authenticated + organization. + parameters: + - default: '"Bearer "' + description: Bearer token + in: header + name: Authorization + required: true + type: string + - default: 20 + description: The maximum number of items to return + in: query + name: limit + type: integer + - description: A cursor for use in pagination. The ID of the last object from + the previous page + in: query + name: after + type: string + - description: Whether to include archived projects. + in: query + name: include_archived + type: string + responses: + "200": + description: Successfully retrieved the list of projects + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectListResponse' + "401": + description: Unauthorized - invalid or missing API key + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: List Projects + tags: + - Projects + post: + consumes: + - application/json + description: Creates a new project for an organization. + parameters: + - default: '"Bearer "' + description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Project creation request + in: body + name: body + required: true + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.CreateProjectRequest' + produces: + - application/json + responses: + "200": + description: Successfully created project + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse' + "400": + description: Bad request - invalid payload + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "401": + description: Unauthorized - invalid or missing API key + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Create Project + tags: + - Projects + /v1/organization/projects/{project_id}: + get: + description: Retrieves a specific project by its ID. + parameters: + - default: '"Bearer "' + description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: ID of the project + in: path + name: project_id + required: true + type: string + responses: + "200": + description: Successfully retrieved the project + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse' + "401": + description: Unauthorized - invalid or missing API key + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Not Found - project with the given ID does not exist or does + not belong to the organization + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Get Project + tags: + - Projects + post: + consumes: + - application/json + description: Updates a specific project by its ID. + parameters: + - default: '"Bearer "' + description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: ID of the project to update + in: path + name: project_id + required: true + type: string + - description: Project update request + in: body + name: body + required: true + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.UpdateProjectRequest' + produces: + - application/json + responses: + "200": + description: Successfully updated the project + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse' + "400": + description: Bad request - invalid payload + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "401": + description: Unauthorized - invalid or missing API key + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Not Found - project with the given ID does not exist + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Update Project + tags: + - Projects + /v1/organization/projects/{project_id}/archive: + post: + description: Archives a specific project by its ID, making it inactive. + parameters: + - default: '"Bearer "' + description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: ID of the project to archive + in: path + name: project_id + required: true + type: string + responses: + "200": + description: Successfully archived the project + schema: + $ref: '#/definitions/app_interfaces_http_routes_v1_organization_projects.ProjectResponse' + "401": + description: Unauthorized - invalid or missing API key + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + "404": + description: Not Found - project with the given ID does not exist + schema: + $ref: '#/definitions/menlo_ai_jan-api-gateway_app_interfaces_http_responses.ErrorResponse' + security: + - BearerAuth: [] + summary: Archive Project + tags: + - Projects /v1/version: get: description: Returns the current build version of the API server. From c1c2306bdd8abe79d737f5fd168fbaa22bd3b134 Mon Sep 17 00:00:00 2001 From: jjchen01 Date: Fri, 29 Aug 2025 13:19:14 +0800 Subject: [PATCH 2/2] chore: update uuid gen --- .../app/domain/project/project_service.go | 13 ++++--------- .../app/utils/stringutils/stringutils.go | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 apps/jan-api-gateway/application/app/utils/stringutils/stringutils.go diff --git a/apps/jan-api-gateway/application/app/domain/project/project_service.go b/apps/jan-api-gateway/application/app/domain/project/project_service.go index b52fabc3..cd5d0438 100644 --- a/apps/jan-api-gateway/application/app/domain/project/project_service.go +++ b/apps/jan-api-gateway/application/app/domain/project/project_service.go @@ -2,12 +2,10 @@ package project import ( "context" - "crypto/rand" - "encoding/base64" "fmt" - "io" "menlo.ai/jan-api-gateway/app/domain/query" + "menlo.ai/jan-api-gateway/app/utils/stringutils" ) // ProjectService provides business logic for managing projects. @@ -24,14 +22,11 @@ func NewService(repo ProjectRepository) *ProjectService { // createPublicID generates a unique, URL-safe public ID for the project. func (s *ProjectService) createPublicID() (string, error) { - randomBytes := make([]byte, 16) - _, err := io.ReadFull(rand.Reader, randomBytes) + randomStr, err := stringutils.RandomString(16) if err != nil { - return "", fmt.Errorf("failed to generate random bytes for public ID: %w", err) + return "", err } - - publicID := base64.URLEncoding.EncodeToString(randomBytes) - return publicID, nil + return fmt.Sprintf("proj_%s", randomStr), nil } // CreateProjectWithPublicID creates a new project and automatically diff --git a/apps/jan-api-gateway/application/app/utils/stringutils/stringutils.go b/apps/jan-api-gateway/application/app/utils/stringutils/stringutils.go new file mode 100644 index 00000000..d40eb4bd --- /dev/null +++ b/apps/jan-api-gateway/application/app/utils/stringutils/stringutils.go @@ -0,0 +1,15 @@ +package stringutils + +import ( + "crypto/rand" + "encoding/base64" + "strings" +) + +func RandomString(n int) (string, error) { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "", err + } + return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "="), nil +}