diff --git a/.github/workflows/api-swagger.yml b/.github/workflows/api-swagger.yml new file mode 100644 index 0000000..e07f69d --- /dev/null +++ b/.github/workflows/api-swagger.yml @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2023 Siemens AG +# SPDX-FileContributor: Gaurav Mishra +# SPDX-License-Identifier: GPL-2.0-only + +name: Swagger API Updated + +on: + pull_request: + branches: [ "main" ] + workflow_dispatch: + +jobs: + + doc-diff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + check-latest: true + cache: true + + - name: Install swag + run: | + go install github.com/swaggo/swag/cmd/swag@latest + + - name: Build Swagger documents + run: | + swag init --generalInfo api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils --output ./swag/docs + + - name: Check doc diff + run: | + diff swag/docs/docs.go cmd/laas/docs/docs.go > swagger-diff.txt + # Check if file swagger-diff.txt is empty + if [ -s swagger-diff.txt ] + then + # If file is not empty, echo a message and exit 1 + echo "Swagger docs are not up to date. Please run 'swag init' and commit the changes." + exit 1 + fi + + doc-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + check-latest: true + cache: true + + - name: Install swag + run: | + go install github.com/swaggo/swag/cmd/swag@latest + + - name: Format Swagger documents + run: | + swag fmt --generalInfo ./pkg/api/api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils + + - name: Check file diff + run: | + git diff --exit-code diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..77b007f --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,9 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: LicenseDB +Source: https://github.com/fossology/LicenseDB/ +Upstream-Contact: FOSSology +Disclaimer: This file is offered as-is, without any warranty. + +Files: cmd/laas/docs/* +Copyright: Fossology contributors +License: GPL-2.0-only diff --git a/README.md b/README.md index 3ebb37b..a9634fc 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,19 @@ go build ./cmd/laas ```bash go run ./cmd/laas ``` + +### Generating Swagger Documentation +1. Install [swag](https://github.com/swaggo/swag) using the following command. + ```bash + go install github.com/swaggo/swag/cmd/swag@latest + ``` +2. Run the following command to generate swagger documentation. + ```bash + swag init --generalInfo api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils --output ./cmd/laas/docs + ``` +3. Swagger documentation will be generated in `./cmd/laas/docs` folder. +4. Run the project and navigate to `http://localhost:8080/swagger/index.html` to view the documentation. +5. Optionally, after changing any documentation comments, format them with following command. + ```bash + swag fmt --generalInfo ./pkg/api/api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils + ``` diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go new file mode 100644 index 0000000..9551543 --- /dev/null +++ b/cmd/laas/docs/docs.go @@ -0,0 +1,1440 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "FOSSology", + "url": "https://fossology.org", + "email": "fossology@fossology.org" + }, + "license": { + "name": "GPL-2.0-only", + "url": "https://github.com/fossology/LicenseDb/blob/main/LICENSE" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/audits": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get all audit records from the server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get audit records", + "operationId": "GetAllAudit", + "responses": { + "200": { + "description": "Audit records", + "schema": { + "$ref": "#/definitions/models.AuditResponse" + } + }, + "404": { + "description": "Not changelogs in DB", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/audits/{audit_id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get a specific audit records by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get an audit record", + "operationId": "GetAudit", + "parameters": [ + { + "type": "string", + "description": "Audit ID", + "name": "audit_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.AuditResponse" + } + }, + "400": { + "description": "Invalid audit ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No audit entry with given ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/audits/{audit_id}/changes": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get changelogs of an audit record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get changelogs", + "operationId": "GetChangeLogs", + "parameters": [ + { + "type": "string", + "description": "Audit ID", + "name": "audit_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChangeLogResponse" + } + }, + "400": { + "description": "Invalid audit ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No audit entry with given ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/audits/{audit_id}/changes/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get a specific changelog of an audit record by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get a changelog", + "operationId": "GetChangeLogbyId", + "parameters": [ + { + "type": "string", + "description": "Audit ID", + "name": "audit_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Changelog ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChangeLogResponse" + } + }, + "400": { + "description": "Invalid ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No changelog with given ID found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/licenses": { + "get": { + "description": "Filter licenses based on different parameters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Filter licenses", + "operationId": "FilterLicense", + "parameters": [ + { + "type": "string", + "description": "SPDX ID of the license", + "name": "spdxid", + "in": "query" + }, + { + "type": "integer", + "description": "License detector type", + "name": "detector_type", + "in": "query" + }, + { + "type": "boolean", + "description": "GPLv2 compatibility flag status of license", + "name": "gplv2compatible", + "in": "query" + }, + { + "type": "boolean", + "description": "GPLv3 compatibility flag status of license", + "name": "gplv3compatible", + "in": "query" + }, + { + "type": "boolean", + "description": "Mary done flag status of license", + "name": "marydone", + "in": "query" + }, + { + "type": "boolean", + "description": "Active license only", + "name": "active", + "in": "query" + }, + { + "type": "boolean", + "description": "OSI Approved flag status of license", + "name": "osiapproved", + "in": "query" + }, + { + "type": "boolean", + "description": "FSF Free flag status of license", + "name": "fsffree", + "in": "query" + }, + { + "type": "boolean", + "description": "Copyleft flag status of license", + "name": "copyleft", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Filtered licenses", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid value", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Create a new license in the service", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Create a new license", + "operationId": "CreateLicense", + "parameters": [ + { + "description": "New license to be created", + "name": "license", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseInput" + } + } + ], + "responses": { + "201": { + "description": "New license created successfully", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "License with same shortname already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Failed to create license", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/licenses/{shortname}": { + "get": { + "description": "Get a single license by its shortname", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Get a license by shortname", + "operationId": "GetLicense", + "parameters": [ + { + "type": "string", + "description": "Shortname of the license", + "name": "shortname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "404": { + "description": "License with shortname not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Update a license in the service", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Update a license", + "operationId": "UpdateLicense", + "parameters": [ + { + "type": "string", + "description": "Shortname of the license to be updated", + "name": "shortname", + "in": "path", + "required": true + }, + { + "description": "Update license body", + "name": "license", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseDB" + } + } + ], + "responses": { + "200": { + "description": "License updated successfully", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid license body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "License with shortname not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "License with same shortname already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Failed to update license", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/obligations": { + "get": { + "description": "Get all active obligations from the service", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Get all active obligations", + "operationId": "GetAllObligation", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "404": { + "description": "No obligations in DB", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Create an obligation and associate it with licenses", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Create an obligation", + "operationId": "CreateObligation", + "parameters": [ + { + "description": "Obligation to create", + "name": "obligation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ObligationInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "400": { + "description": "Bad request body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "Obligation with same body exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Unable to create obligation", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/obligations/{topic}": { + "get": { + "description": "Get an active based on given topic", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Get an obligation", + "operationId": "GetObligation", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation", + "name": "topic", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "404": { + "description": "No obligation with given topic found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Deactivate an obligation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Deactivate obligation", + "operationId": "DeleteObligation", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation to be updated", + "name": "topic", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "No obligation with given topic found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Update an existing obligation record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Update obligation", + "operationId": "UpdateObligation", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation to be updated", + "name": "topic", + "in": "path", + "required": true + }, + { + "description": "Obligation to be updated", + "name": "obligation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateObligation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No obligation with given topic found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Unable to update obligation", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/search": { + "post": { + "description": "Search licenses on different filters and algorithms", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Search licenses", + "operationId": "SearchInLicense", + "parameters": [ + { + "description": "Search criteria", + "name": "search", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SearchLicense" + } + } + ], + "responses": { + "200": { + "description": "Licenses matched", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "Search algorithm doesn't exist", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get all service users", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get users", + "operationId": "GetAllUsers", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "404": { + "description": "Users not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Create a new service user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create new user", + "operationId": "CreateUser", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "User already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get a single user by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get a user", + "operationId": "GetUser", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid user id", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "User not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + } + }, + "definitions": { + "models.Audit": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 456 + }, + "timestamp": { + "type": "string", + "example": "2023-12-01T18:10:25.00+05:30" + }, + "type": { + "type": "string", + "example": "license" + }, + "type_id": { + "type": "integer", + "example": 34 + }, + "user_id": { + "type": "integer", + "example": 123 + } + } + }, + "models.AuditResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Audit" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.ChangeLog": { + "type": "object", + "properties": { + "audit_id": { + "type": "integer", + "example": 456 + }, + "field": { + "type": "string", + "example": "rf_text" + }, + "id": { + "type": "integer", + "example": 789 + }, + "old_value": { + "type": "string", + "example": "Old license text" + }, + "updated_value": { + "type": "string", + "example": "New license text" + } + } + }, + "models.ChangeLogResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChangeLog" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.LicenseDB": { + "type": "object", + "required": [ + "rf_shortname" + ], + "properties": { + "marydone": { + "type": "boolean" + }, + "rf_FSFfree": { + "type": "boolean" + }, + "rf_Fedora": { + "type": "string" + }, + "rf_GPLv2compatible": { + "type": "boolean" + }, + "rf_GPLv3compatible": { + "type": "boolean" + }, + "rf_OSIapproved": { + "type": "boolean" + }, + "rf_active": { + "type": "boolean" + }, + "rf_add_date": { + "type": "string", + "example": "2023-12-01T18:10:25.00+05:30" + }, + "rf_copyleft": { + "type": "boolean" + }, + "rf_detector_type": { + "type": "integer", + "example": 1 + }, + "rf_flag": { + "type": "integer", + "example": 1 + }, + "rf_fullname": { + "type": "string", + "example": "MIT License" + }, + "rf_id": { + "type": "integer", + "example": 123 + }, + "rf_notes": { + "type": "string", + "example": "This license has been superseded." + }, + "rf_risk": { + "type": "integer" + }, + "rf_shortname": { + "type": "string", + "example": "MIT" + }, + "rf_source": { + "type": "string" + }, + "rf_spdx_id": { + "type": "string", + "example": "MIT" + }, + "rf_text": { + "type": "string", + "example": "MIT License Text here" + }, + "rf_text_updatable": { + "type": "boolean" + }, + "rf_url": { + "type": "string", + "example": "https://opensource.org/licenses/MIT" + } + } + }, + "models.LicenseError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "invalid request body" + }, + "message": { + "type": "string", + "example": "invalid request body" + }, + "path": { + "type": "string", + "example": "/api/v1/licenses" + }, + "status": { + "type": "integer", + "example": 400 + }, + "timestamp": { + "type": "string", + "example": "2023-12-01T10:00:51+05:30" + } + } + }, + "models.LicenseInput": { + "type": "object", + "properties": { + "marydone": { + "type": "boolean" + }, + "rf_FSFfree": { + "type": "boolean" + }, + "rf_Fedora": { + "type": "string" + }, + "rf_GPLv2compatible": { + "type": "boolean" + }, + "rf_GPLv3compatible": { + "type": "boolean" + }, + "rf_OSIapproved": { + "type": "boolean" + }, + "rf_active": { + "type": "boolean" + }, + "rf_add_date": { + "type": "string", + "example": "2023-12-01T18:10:25.00+05:30" + }, + "rf_copyleft": { + "type": "boolean" + }, + "rf_detector_type": { + "type": "integer", + "example": 1 + }, + "rf_flag": { + "type": "integer", + "example": 1 + }, + "rf_fullname": { + "type": "string", + "example": "MIT License" + }, + "rf_notes": { + "type": "string", + "example": "This license has been superseded." + }, + "rf_risk": { + "type": "integer" + }, + "rf_shortname": { + "type": "string", + "example": "MIT" + }, + "rf_source": { + "type": "string" + }, + "rf_spdx_id": { + "type": "string", + "example": "MIT" + }, + "rf_text": { + "type": "string", + "example": "MIT License Text here" + }, + "rf_text_updatable": { + "type": "boolean" + }, + "rf_url": { + "type": "string", + "example": "https://opensource.org/licenses/MIT" + } + } + }, + "models.LicenseResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LicenseDB" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.Obligation": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "classification": { + "type": "string", + "enum": [ + "green", + "white", + "yellow", + "red" + ] + }, + "comment": { + "type": "string", + "example": "This is a comment." + }, + "id": { + "type": "integer", + "example": 147 + }, + "md5": { + "type": "string", + "example": "deadbeef" + }, + "modifications": { + "type": "boolean" + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean" + }, + "topic": { + "type": "string", + "example": "copyleft" + }, + "type": { + "type": "string", + "enum": [ + "obligation", + "restriction", + "risk", + "right" + ] + } + } + }, + "models.ObligationInput": { + "type": "object", + "required": [ + "text", + "topic", + "type" + ], + "properties": { + "active": { + "type": "boolean", + "example": true + }, + "classification": { + "type": "string", + "enum": [ + "green", + "white", + "yellow", + "red" + ] + }, + "comment": { + "type": "string", + "example": "This is a comment." + }, + "modifications": { + "type": "boolean" + }, + "shortnames": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "GPL-2.0-only", + "GPL-2.0-or-later" + ] + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean" + }, + "topic": { + "type": "string", + "example": "copyleft" + }, + "type": { + "type": "string", + "enum": [ + "obligation", + "restriction", + "risk", + "right" + ] + } + } + }, + "models.ObligationResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Obligation" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.PaginationMeta": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 10 + }, + "resource_count": { + "type": "integer", + "example": 20 + } + } + }, + "models.SearchLicense": { + "type": "object", + "required": [ + "field", + "search_term" + ], + "properties": { + "field": { + "type": "string", + "example": "rf_text" + }, + "search": { + "type": "string", + "enum": [ + "fuzzy", + "full_text_search" + ] + }, + "search_term": { + "type": "string", + "example": "MIT License" + } + } + }, + "models.UpdateObligation": { + "type": "object", + "required": [ + "text", + "topic", + "type" + ], + "properties": { + "active": { + "type": "boolean", + "example": true + }, + "classification": { + "type": "string", + "enum": [ + "green", + "white", + "yellow", + "red" + ] + }, + "comment": { + "type": "string", + "example": "This is a comment." + }, + "modifications": { + "type": "boolean" + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean" + }, + "topic": { + "type": "string", + "example": "copyleft" + }, + "type": { + "type": "string", + "enum": [ + "obligation", + "restriction", + "risk", + "right" + ] + } + } + }, + "models.User": { + "type": "object", + "required": [ + "userlevel", + "username", + "userpassword" + ], + "properties": { + "id": { + "type": "integer", + "example": 123 + }, + "userlevel": { + "type": "string", + "example": "admin" + }, + "username": { + "type": "string", + "example": "fossy" + }, + "userpassword": { + "type": "string" + } + } + }, + "models.UserResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.0.9", + Host: "localhost:8080", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "laas (License as a Service) API", + Description: "Service to host license information for other services to query over REST API.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json new file mode 100644 index 0000000..81b7111 --- /dev/null +++ b/cmd/laas/docs/swagger.json @@ -0,0 +1,1416 @@ +{ + "swagger": "2.0", + "info": { + "description": "Service to host license information for other services to query over REST API.", + "title": "laas (License as a Service) API", + "contact": { + "name": "FOSSology", + "url": "https://fossology.org", + "email": "fossology@fossology.org" + }, + "license": { + "name": "GPL-2.0-only", + "url": "https://github.com/fossology/LicenseDb/blob/main/LICENSE" + }, + "version": "0.0.9" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/audits": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get all audit records from the server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get audit records", + "operationId": "GetAllAudit", + "responses": { + "200": { + "description": "Audit records", + "schema": { + "$ref": "#/definitions/models.AuditResponse" + } + }, + "404": { + "description": "Not changelogs in DB", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/audits/{audit_id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get a specific audit records by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get an audit record", + "operationId": "GetAudit", + "parameters": [ + { + "type": "string", + "description": "Audit ID", + "name": "audit_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.AuditResponse" + } + }, + "400": { + "description": "Invalid audit ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No audit entry with given ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/audits/{audit_id}/changes": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get changelogs of an audit record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get changelogs", + "operationId": "GetChangeLogs", + "parameters": [ + { + "type": "string", + "description": "Audit ID", + "name": "audit_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChangeLogResponse" + } + }, + "400": { + "description": "Invalid audit ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No audit entry with given ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/audits/{audit_id}/changes/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get a specific changelog of an audit record by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audits" + ], + "summary": "Get a changelog", + "operationId": "GetChangeLogbyId", + "parameters": [ + { + "type": "string", + "description": "Audit ID", + "name": "audit_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Changelog ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChangeLogResponse" + } + }, + "400": { + "description": "Invalid ID", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No changelog with given ID found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/licenses": { + "get": { + "description": "Filter licenses based on different parameters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Filter licenses", + "operationId": "FilterLicense", + "parameters": [ + { + "type": "string", + "description": "SPDX ID of the license", + "name": "spdxid", + "in": "query" + }, + { + "type": "integer", + "description": "License detector type", + "name": "detector_type", + "in": "query" + }, + { + "type": "boolean", + "description": "GPLv2 compatibility flag status of license", + "name": "gplv2compatible", + "in": "query" + }, + { + "type": "boolean", + "description": "GPLv3 compatibility flag status of license", + "name": "gplv3compatible", + "in": "query" + }, + { + "type": "boolean", + "description": "Mary done flag status of license", + "name": "marydone", + "in": "query" + }, + { + "type": "boolean", + "description": "Active license only", + "name": "active", + "in": "query" + }, + { + "type": "boolean", + "description": "OSI Approved flag status of license", + "name": "osiapproved", + "in": "query" + }, + { + "type": "boolean", + "description": "FSF Free flag status of license", + "name": "fsffree", + "in": "query" + }, + { + "type": "boolean", + "description": "Copyleft flag status of license", + "name": "copyleft", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Filtered licenses", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid value", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Create a new license in the service", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Create a new license", + "operationId": "CreateLicense", + "parameters": [ + { + "description": "New license to be created", + "name": "license", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseInput" + } + } + ], + "responses": { + "201": { + "description": "New license created successfully", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "License with same shortname already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Failed to create license", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/licenses/{shortname}": { + "get": { + "description": "Get a single license by its shortname", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Get a license by shortname", + "operationId": "GetLicense", + "parameters": [ + { + "type": "string", + "description": "Shortname of the license", + "name": "shortname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "404": { + "description": "License with shortname not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Update a license in the service", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Update a license", + "operationId": "UpdateLicense", + "parameters": [ + { + "type": "string", + "description": "Shortname of the license to be updated", + "name": "shortname", + "in": "path", + "required": true + }, + { + "description": "Update license body", + "name": "license", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LicenseDB" + } + } + ], + "responses": { + "200": { + "description": "License updated successfully", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid license body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "License with shortname not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "License with same shortname already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Failed to update license", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/obligations": { + "get": { + "description": "Get all active obligations from the service", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Get all active obligations", + "operationId": "GetAllObligation", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "404": { + "description": "No obligations in DB", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Create an obligation and associate it with licenses", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Create an obligation", + "operationId": "CreateObligation", + "parameters": [ + { + "description": "Obligation to create", + "name": "obligation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ObligationInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "400": { + "description": "Bad request body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "Obligation with same body exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Unable to create obligation", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/obligations/{topic}": { + "get": { + "description": "Get an active based on given topic", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Get an obligation", + "operationId": "GetObligation", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation", + "name": "topic", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "404": { + "description": "No obligation with given topic found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Deactivate an obligation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Deactivate obligation", + "operationId": "DeleteObligation", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation to be updated", + "name": "topic", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "No obligation with given topic found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Update an existing obligation record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Update obligation", + "operationId": "UpdateObligation", + "parameters": [ + { + "type": "string", + "description": "Topic of the obligation to be updated", + "name": "topic", + "in": "path", + "required": true + }, + { + "description": "Obligation to be updated", + "name": "obligation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateObligation" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ObligationResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "No obligation with given topic found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Unable to update obligation", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/search": { + "post": { + "description": "Search licenses on different filters and algorithms", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Licenses" + ], + "summary": "Search licenses", + "operationId": "SearchInLicense", + "parameters": [ + { + "description": "Search criteria", + "name": "search", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SearchLicense" + } + } + ], + "responses": { + "200": { + "description": "Licenses matched", + "schema": { + "$ref": "#/definitions/models.LicenseResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "Search algorithm doesn't exist", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get all service users", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get users", + "operationId": "GetAllUsers", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "404": { + "description": "Users not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Create a new service user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create new user", + "operationId": "CreateUser", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "User already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get a single user by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get a user", + "operationId": "GetUser", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid user id", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "404": { + "description": "User not found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + } + }, + "definitions": { + "models.Audit": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 456 + }, + "timestamp": { + "type": "string", + "example": "2023-12-01T18:10:25.00+05:30" + }, + "type": { + "type": "string", + "example": "license" + }, + "type_id": { + "type": "integer", + "example": 34 + }, + "user_id": { + "type": "integer", + "example": 123 + } + } + }, + "models.AuditResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Audit" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.ChangeLog": { + "type": "object", + "properties": { + "audit_id": { + "type": "integer", + "example": 456 + }, + "field": { + "type": "string", + "example": "rf_text" + }, + "id": { + "type": "integer", + "example": 789 + }, + "old_value": { + "type": "string", + "example": "Old license text" + }, + "updated_value": { + "type": "string", + "example": "New license text" + } + } + }, + "models.ChangeLogResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChangeLog" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.LicenseDB": { + "type": "object", + "required": [ + "rf_shortname" + ], + "properties": { + "marydone": { + "type": "boolean" + }, + "rf_FSFfree": { + "type": "boolean" + }, + "rf_Fedora": { + "type": "string" + }, + "rf_GPLv2compatible": { + "type": "boolean" + }, + "rf_GPLv3compatible": { + "type": "boolean" + }, + "rf_OSIapproved": { + "type": "boolean" + }, + "rf_active": { + "type": "boolean" + }, + "rf_add_date": { + "type": "string", + "example": "2023-12-01T18:10:25.00+05:30" + }, + "rf_copyleft": { + "type": "boolean" + }, + "rf_detector_type": { + "type": "integer", + "example": 1 + }, + "rf_flag": { + "type": "integer", + "example": 1 + }, + "rf_fullname": { + "type": "string", + "example": "MIT License" + }, + "rf_id": { + "type": "integer", + "example": 123 + }, + "rf_notes": { + "type": "string", + "example": "This license has been superseded." + }, + "rf_risk": { + "type": "integer" + }, + "rf_shortname": { + "type": "string", + "example": "MIT" + }, + "rf_source": { + "type": "string" + }, + "rf_spdx_id": { + "type": "string", + "example": "MIT" + }, + "rf_text": { + "type": "string", + "example": "MIT License Text here" + }, + "rf_text_updatable": { + "type": "boolean" + }, + "rf_url": { + "type": "string", + "example": "https://opensource.org/licenses/MIT" + } + } + }, + "models.LicenseError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "invalid request body" + }, + "message": { + "type": "string", + "example": "invalid request body" + }, + "path": { + "type": "string", + "example": "/api/v1/licenses" + }, + "status": { + "type": "integer", + "example": 400 + }, + "timestamp": { + "type": "string", + "example": "2023-12-01T10:00:51+05:30" + } + } + }, + "models.LicenseInput": { + "type": "object", + "properties": { + "marydone": { + "type": "boolean" + }, + "rf_FSFfree": { + "type": "boolean" + }, + "rf_Fedora": { + "type": "string" + }, + "rf_GPLv2compatible": { + "type": "boolean" + }, + "rf_GPLv3compatible": { + "type": "boolean" + }, + "rf_OSIapproved": { + "type": "boolean" + }, + "rf_active": { + "type": "boolean" + }, + "rf_add_date": { + "type": "string", + "example": "2023-12-01T18:10:25.00+05:30" + }, + "rf_copyleft": { + "type": "boolean" + }, + "rf_detector_type": { + "type": "integer", + "example": 1 + }, + "rf_flag": { + "type": "integer", + "example": 1 + }, + "rf_fullname": { + "type": "string", + "example": "MIT License" + }, + "rf_notes": { + "type": "string", + "example": "This license has been superseded." + }, + "rf_risk": { + "type": "integer" + }, + "rf_shortname": { + "type": "string", + "example": "MIT" + }, + "rf_source": { + "type": "string" + }, + "rf_spdx_id": { + "type": "string", + "example": "MIT" + }, + "rf_text": { + "type": "string", + "example": "MIT License Text here" + }, + "rf_text_updatable": { + "type": "boolean" + }, + "rf_url": { + "type": "string", + "example": "https://opensource.org/licenses/MIT" + } + } + }, + "models.LicenseResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.LicenseDB" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.Obligation": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "classification": { + "type": "string", + "enum": [ + "green", + "white", + "yellow", + "red" + ] + }, + "comment": { + "type": "string", + "example": "This is a comment." + }, + "id": { + "type": "integer", + "example": 147 + }, + "md5": { + "type": "string", + "example": "deadbeef" + }, + "modifications": { + "type": "boolean" + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean" + }, + "topic": { + "type": "string", + "example": "copyleft" + }, + "type": { + "type": "string", + "enum": [ + "obligation", + "restriction", + "risk", + "right" + ] + } + } + }, + "models.ObligationInput": { + "type": "object", + "required": [ + "text", + "topic", + "type" + ], + "properties": { + "active": { + "type": "boolean", + "example": true + }, + "classification": { + "type": "string", + "enum": [ + "green", + "white", + "yellow", + "red" + ] + }, + "comment": { + "type": "string", + "example": "This is a comment." + }, + "modifications": { + "type": "boolean" + }, + "shortnames": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "GPL-2.0-only", + "GPL-2.0-or-later" + ] + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean" + }, + "topic": { + "type": "string", + "example": "copyleft" + }, + "type": { + "type": "string", + "enum": [ + "obligation", + "restriction", + "risk", + "right" + ] + } + } + }, + "models.ObligationResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Obligation" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, + "models.PaginationMeta": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 10 + }, + "resource_count": { + "type": "integer", + "example": 20 + } + } + }, + "models.SearchLicense": { + "type": "object", + "required": [ + "field", + "search_term" + ], + "properties": { + "field": { + "type": "string", + "example": "rf_text" + }, + "search": { + "type": "string", + "enum": [ + "fuzzy", + "full_text_search" + ] + }, + "search_term": { + "type": "string", + "example": "MIT License" + } + } + }, + "models.UpdateObligation": { + "type": "object", + "required": [ + "text", + "topic", + "type" + ], + "properties": { + "active": { + "type": "boolean", + "example": true + }, + "classification": { + "type": "string", + "enum": [ + "green", + "white", + "yellow", + "red" + ] + }, + "comment": { + "type": "string", + "example": "This is a comment." + }, + "modifications": { + "type": "boolean" + }, + "text": { + "type": "string", + "example": "Source code be made available when distributing the software." + }, + "text_updatable": { + "type": "boolean" + }, + "topic": { + "type": "string", + "example": "copyleft" + }, + "type": { + "type": "string", + "enum": [ + "obligation", + "restriction", + "risk", + "right" + ] + } + } + }, + "models.User": { + "type": "object", + "required": [ + "userlevel", + "username", + "userpassword" + ], + "properties": { + "id": { + "type": "integer", + "example": 123 + }, + "userlevel": { + "type": "string", + "example": "admin" + }, + "username": { + "type": "string", + "example": "fossy" + }, + "userpassword": { + "type": "string" + } + } + }, + "models.UserResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + }, + "paginationmeta": { + "$ref": "#/definitions/models.PaginationMeta" + }, + "status": { + "type": "integer", + "example": 200 + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + } +} \ No newline at end of file diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml new file mode 100644 index 0000000..8ce0ae0 --- /dev/null +++ b/cmd/laas/docs/swagger.yaml @@ -0,0 +1,954 @@ +basePath: /api/v1 +definitions: + models.Audit: + properties: + id: + example: 456 + type: integer + timestamp: + example: "2023-12-01T18:10:25.00+05:30" + type: string + type: + example: license + type: string + type_id: + example: 34 + type: integer + user_id: + example: 123 + type: integer + type: object + models.AuditResponse: + properties: + data: + items: + $ref: '#/definitions/models.Audit' + type: array + paginationmeta: + $ref: '#/definitions/models.PaginationMeta' + status: + example: 200 + type: integer + type: object + models.ChangeLog: + properties: + audit_id: + example: 456 + type: integer + field: + example: rf_text + type: string + id: + example: 789 + type: integer + old_value: + example: Old license text + type: string + updated_value: + example: New license text + type: string + type: object + models.ChangeLogResponse: + properties: + data: + items: + $ref: '#/definitions/models.ChangeLog' + type: array + paginationmeta: + $ref: '#/definitions/models.PaginationMeta' + status: + example: 200 + type: integer + type: object + models.LicenseDB: + properties: + marydone: + type: boolean + rf_FSFfree: + type: boolean + rf_Fedora: + type: string + rf_GPLv2compatible: + type: boolean + rf_GPLv3compatible: + type: boolean + rf_OSIapproved: + type: boolean + rf_active: + type: boolean + rf_add_date: + example: "2023-12-01T18:10:25.00+05:30" + type: string + rf_copyleft: + type: boolean + rf_detector_type: + example: 1 + type: integer + rf_flag: + example: 1 + type: integer + rf_fullname: + example: MIT License + type: string + rf_id: + example: 123 + type: integer + rf_notes: + example: This license has been superseded. + type: string + rf_risk: + type: integer + rf_shortname: + example: MIT + type: string + rf_source: + type: string + rf_spdx_id: + example: MIT + type: string + rf_text: + example: MIT License Text here + type: string + rf_text_updatable: + type: boolean + rf_url: + example: https://opensource.org/licenses/MIT + type: string + required: + - rf_shortname + type: object + models.LicenseError: + properties: + error: + example: invalid request body + type: string + message: + example: invalid request body + type: string + path: + example: /api/v1/licenses + type: string + status: + example: 400 + type: integer + timestamp: + example: "2023-12-01T10:00:51+05:30" + type: string + type: object + models.LicenseInput: + properties: + marydone: + type: boolean + rf_FSFfree: + type: boolean + rf_Fedora: + type: string + rf_GPLv2compatible: + type: boolean + rf_GPLv3compatible: + type: boolean + rf_OSIapproved: + type: boolean + rf_active: + type: boolean + rf_add_date: + example: "2023-12-01T18:10:25.00+05:30" + type: string + rf_copyleft: + type: boolean + rf_detector_type: + example: 1 + type: integer + rf_flag: + example: 1 + type: integer + rf_fullname: + example: MIT License + type: string + rf_notes: + example: This license has been superseded. + type: string + rf_risk: + type: integer + rf_shortname: + example: MIT + type: string + rf_source: + type: string + rf_spdx_id: + example: MIT + type: string + rf_text: + example: MIT License Text here + type: string + rf_text_updatable: + type: boolean + rf_url: + example: https://opensource.org/licenses/MIT + type: string + type: object + models.LicenseResponse: + properties: + data: + items: + $ref: '#/definitions/models.LicenseDB' + type: array + paginationmeta: + $ref: '#/definitions/models.PaginationMeta' + status: + example: 200 + type: integer + type: object + models.Obligation: + properties: + active: + type: boolean + classification: + enum: + - green + - white + - yellow + - red + type: string + comment: + example: This is a comment. + type: string + id: + example: 147 + type: integer + md5: + example: deadbeef + type: string + modifications: + type: boolean + text: + example: Source code be made available when distributing the software. + type: string + text_updatable: + type: boolean + topic: + example: copyleft + type: string + type: + enum: + - obligation + - restriction + - risk + - right + type: string + type: object + models.ObligationInput: + properties: + active: + example: true + type: boolean + classification: + enum: + - green + - white + - yellow + - red + type: string + comment: + example: This is a comment. + type: string + modifications: + type: boolean + shortnames: + example: + - GPL-2.0-only + - GPL-2.0-or-later + items: + type: string + type: array + text: + example: Source code be made available when distributing the software. + type: string + text_updatable: + type: boolean + topic: + example: copyleft + type: string + type: + enum: + - obligation + - restriction + - risk + - right + type: string + required: + - text + - topic + - type + type: object + models.ObligationResponse: + properties: + data: + items: + $ref: '#/definitions/models.Obligation' + type: array + paginationmeta: + $ref: '#/definitions/models.PaginationMeta' + status: + example: 200 + type: integer + type: object + models.PaginationMeta: + properties: + page: + example: 1 + type: integer + per_page: + example: 10 + type: integer + resource_count: + example: 20 + type: integer + type: object + models.SearchLicense: + properties: + field: + example: rf_text + type: string + search: + enum: + - fuzzy + - full_text_search + type: string + search_term: + example: MIT License + type: string + required: + - field + - search_term + type: object + models.UpdateObligation: + properties: + active: + example: true + type: boolean + classification: + enum: + - green + - white + - yellow + - red + type: string + comment: + example: This is a comment. + type: string + modifications: + type: boolean + text: + example: Source code be made available when distributing the software. + type: string + text_updatable: + type: boolean + topic: + example: copyleft + type: string + type: + enum: + - obligation + - restriction + - risk + - right + type: string + required: + - text + - topic + - type + type: object + models.User: + properties: + id: + example: 123 + type: integer + userlevel: + example: admin + type: string + username: + example: fossy + type: string + userpassword: + type: string + required: + - userlevel + - username + - userpassword + type: object + models.UserResponse: + properties: + data: + items: + $ref: '#/definitions/models.User' + type: array + paginationmeta: + $ref: '#/definitions/models.PaginationMeta' + status: + example: 200 + type: integer + type: object +host: localhost:8080 +info: + contact: + email: fossology@fossology.org + name: FOSSology + url: https://fossology.org + description: Service to host license information for other services to query over + REST API. + license: + name: GPL-2.0-only + url: https://github.com/fossology/LicenseDb/blob/main/LICENSE + title: laas (License as a Service) API + version: 0.0.9 +paths: + /audits: + get: + consumes: + - application/json + description: Get all audit records from the server + operationId: GetAllAudit + produces: + - application/json + responses: + "200": + description: Audit records + schema: + $ref: '#/definitions/models.AuditResponse' + "404": + description: Not changelogs in DB + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Get audit records + tags: + - Audits + /audits/{audit_id}: + get: + consumes: + - application/json + description: Get a specific audit records by ID + operationId: GetAudit + parameters: + - description: Audit ID + in: path + name: audit_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.AuditResponse' + "400": + description: Invalid audit ID + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: No audit entry with given ID + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Get an audit record + tags: + - Audits + /audits/{audit_id}/changes: + get: + consumes: + - application/json + description: Get changelogs of an audit record + operationId: GetChangeLogs + parameters: + - description: Audit ID + in: path + name: audit_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ChangeLogResponse' + "400": + description: Invalid audit ID + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: No audit entry with given ID + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Get changelogs + tags: + - Audits + /audits/{audit_id}/changes/{id}: + get: + consumes: + - application/json + description: Get a specific changelog of an audit record by its ID + operationId: GetChangeLogbyId + parameters: + - description: Audit ID + in: path + name: audit_id + required: true + type: string + - description: Changelog ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ChangeLogResponse' + "400": + description: Invalid ID + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: No changelog with given ID found + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Get a changelog + tags: + - Audits + /licenses: + get: + consumes: + - application/json + description: Filter licenses based on different parameters + operationId: FilterLicense + parameters: + - description: SPDX ID of the license + in: query + name: spdxid + type: string + - description: License detector type + in: query + name: detector_type + type: integer + - description: GPLv2 compatibility flag status of license + in: query + name: gplv2compatible + type: boolean + - description: GPLv3 compatibility flag status of license + in: query + name: gplv3compatible + type: boolean + - description: Mary done flag status of license + in: query + name: marydone + type: boolean + - description: Active license only + in: query + name: active + type: boolean + - description: OSI Approved flag status of license + in: query + name: osiapproved + type: boolean + - description: FSF Free flag status of license + in: query + name: fsffree + type: boolean + - description: Copyleft flag status of license + in: query + name: copyleft + type: boolean + produces: + - application/json + responses: + "200": + description: Filtered licenses + schema: + $ref: '#/definitions/models.LicenseResponse' + "400": + description: Invalid value + schema: + $ref: '#/definitions/models.LicenseError' + summary: Filter licenses + tags: + - Licenses + post: + consumes: + - application/json + description: Create a new license in the service + operationId: CreateLicense + parameters: + - description: New license to be created + in: body + name: license + required: true + schema: + $ref: '#/definitions/models.LicenseInput' + produces: + - application/json + responses: + "201": + description: New license created successfully + schema: + $ref: '#/definitions/models.LicenseResponse' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.LicenseError' + "409": + description: License with same shortname already exists + schema: + $ref: '#/definitions/models.LicenseError' + "500": + description: Failed to create license + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Create a new license + tags: + - Licenses + /licenses/{shortname}: + get: + consumes: + - application/json + description: Get a single license by its shortname + operationId: GetLicense + parameters: + - description: Shortname of the license + in: path + name: shortname + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.LicenseResponse' + "404": + description: License with shortname not found + schema: + $ref: '#/definitions/models.LicenseError' + summary: Get a license by shortname + tags: + - Licenses + patch: + consumes: + - application/json + description: Update a license in the service + operationId: UpdateLicense + parameters: + - description: Shortname of the license to be updated + in: path + name: shortname + required: true + type: string + - description: Update license body + in: body + name: license + required: true + schema: + $ref: '#/definitions/models.LicenseDB' + produces: + - application/json + responses: + "200": + description: License updated successfully + schema: + $ref: '#/definitions/models.LicenseResponse' + "400": + description: Invalid license body + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: License with shortname not found + schema: + $ref: '#/definitions/models.LicenseError' + "409": + description: License with same shortname already exists + schema: + $ref: '#/definitions/models.LicenseError' + "500": + description: Failed to update license + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Update a license + tags: + - Licenses + /obligations: + get: + consumes: + - application/json + description: Get all active obligations from the service + operationId: GetAllObligation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ObligationResponse' + "404": + description: No obligations in DB + schema: + $ref: '#/definitions/models.LicenseError' + summary: Get all active obligations + tags: + - Obligations + post: + consumes: + - application/json + description: Create an obligation and associate it with licenses + operationId: CreateObligation + parameters: + - description: Obligation to create + in: body + name: obligation + required: true + schema: + $ref: '#/definitions/models.ObligationInput' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.ObligationResponse' + "400": + description: Bad request body + schema: + $ref: '#/definitions/models.LicenseError' + "409": + description: Obligation with same body exists + schema: + $ref: '#/definitions/models.LicenseError' + "500": + description: Unable to create obligation + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Create an obligation + tags: + - Obligations + /obligations/{topic}: + delete: + consumes: + - application/json + description: Deactivate an obligation + operationId: DeleteObligation + parameters: + - description: Topic of the obligation to be updated + in: path + name: topic + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: No obligation with given topic found + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Deactivate obligation + tags: + - Obligations + get: + consumes: + - application/json + description: Get an active based on given topic + operationId: GetObligation + parameters: + - description: Topic of the obligation + in: path + name: topic + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ObligationResponse' + "404": + description: No obligation with given topic found + schema: + $ref: '#/definitions/models.LicenseError' + summary: Get an obligation + tags: + - Obligations + patch: + consumes: + - application/json + description: Update an existing obligation record + operationId: UpdateObligation + parameters: + - description: Topic of the obligation to be updated + in: path + name: topic + required: true + type: string + - description: Obligation to be updated + in: body + name: obligation + required: true + schema: + $ref: '#/definitions/models.UpdateObligation' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ObligationResponse' + "400": + description: Invalid request + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: No obligation with given topic found + schema: + $ref: '#/definitions/models.LicenseError' + "500": + description: Unable to update obligation + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Update obligation + tags: + - Obligations + /search: + post: + consumes: + - application/json + description: Search licenses on different filters and algorithms + operationId: SearchInLicense + parameters: + - description: Search criteria + in: body + name: search + required: true + schema: + $ref: '#/definitions/models.SearchLicense' + produces: + - application/json + responses: + "200": + description: Licenses matched + schema: + $ref: '#/definitions/models.LicenseResponse' + "400": + description: Invalid request + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: Search algorithm doesn't exist + schema: + $ref: '#/definitions/models.LicenseError' + summary: Search licenses + tags: + - Licenses + /users: + get: + consumes: + - application/json + description: Get all service users + operationId: GetAllUsers + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserResponse' + "404": + description: Users not found + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Get users + tags: + - Users + post: + consumes: + - application/json + description: Create a new service user + operationId: CreateUser + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.UserResponse' + "400": + description: Invalid json body + schema: + $ref: '#/definitions/models.LicenseError' + "409": + description: User already exists + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Create new user + tags: + - Users + /users/{id}: + get: + consumes: + - application/json + description: Get a single user by ID + operationId: GetUser + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserResponse' + "400": + description: Invalid user id + schema: + $ref: '#/definitions/models.LicenseError' + "404": + description: User not found + schema: + $ref: '#/definitions/models.LicenseError' + security: + - BasicAuth: [] + summary: Get a user + tags: + - Users +securityDefinitions: + BasicAuth: + type: basic +swagger: "2.0" diff --git a/cmd/laas/main.go b/cmd/laas/main.go index 8a070bf..11f98ce 100644 --- a/cmd/laas/main.go +++ b/cmd/laas/main.go @@ -1,14 +1,19 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla +// SPDX-FileCopyrightText: 2023 Siemens AG +// SPDX-FileContributor: Gaurav Mishra +// // SPDX-License-Identifier: GPL-2.0-only package main import ( "flag" + "log" + + _ "github.com/fossology/LicenseDb/cmd/laas/docs" "github.com/fossology/LicenseDb/pkg/api" "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" - "log" ) // declare flags to input the basic requirement of database connection and the path of the data file diff --git a/go.mod b/go.mod index e38e14c..ec664a7 100644 --- a/go.mod +++ b/go.mod @@ -5,35 +5,46 @@ go 1.20 require ( github.com/gin-gonic/gin v1.9.1 github.com/stretchr/testify v1.8.3 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.2 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.1 ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.3.1 // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect @@ -41,6 +52,8 @@ require ( golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 20722a2..d4a0438 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -10,10 +16,21 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -31,22 +48,31 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= -github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -54,16 +80,18 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -71,31 +99,76 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= diff --git a/pkg/api/api.go b/pkg/api/api.go index 23a8c41..07b7534 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -1,4 +1,7 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla +// SPDX-FileCopyrightText: 2023 Siemens AG +// SPDX-FileContributor: Gaurav Mishra +// // SPDX-License-Identifier: GPL-2.0-only package api @@ -11,12 +14,33 @@ import ( "strconv" "time" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "github.com/fossology/LicenseDb/pkg/auth" "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" - "github.com/gin-gonic/gin" + "github.com/fossology/LicenseDb/pkg/utils" ) +// Router Get the gin router with all the routes defined +// +// @title laas (License as a Service) API +// @version 0.0.9 +// @description Service to host license information for other services to query over REST API. +// +// @contact.name FOSSology +// @contact.url https://fossology.org +// @contact.email fossology@fossology.org +// +// @license.name GPL-2.0-only +// @license.url https://github.com/fossology/LicenseDb/blob/main/LICENSE +// +// @host localhost:8080 +// @BasePath /api/v1 +// +// @securityDefinitions.basic BasicAuth func Router() *gin.Engine { // r is a default instance of gin engine r := gin.Default() @@ -24,31 +48,55 @@ func Router() *gin.Engine { // return error for invalid routes r.NoRoute(HandleInvalidUrl) - // authorization not required for these routes - r.GET("/api/licenses/:shortname", GetLicense) - r.POST("/api/search", SearchInLicense) - r.GET("/api/licenses", FilterLicense) - - // set up authentication - authorized := r.Group("/") - authorized.Use(auth.AuthenticationMiddleware()) - - authorized.POST("/api/licenses", CreateLicense) - authorized.PATCH("/api/licenses/:shortname", UpdateLicense) - authorized.POST("/api/users", auth.CreateUser) - authorized.GET("/api/users", auth.GetAllUser) - authorized.GET("/api/users/:id", auth.GetUser) + unAuthorizedv1 := r.Group("/api/v1") + { + licenses := unAuthorizedv1.Group("/licenses") + { + licenses.GET("", FilterLicense) + licenses.GET(":shortname", GetLicense) + } + search := unAuthorizedv1.Group("/search") + { + search.POST("", SearchInLicense) + } + obligations := unAuthorizedv1.Group("/obligations") + { + obligations.GET("", GetAllObligation) + obligations.GET(":topic", GetObligation) + } + } - authorized.GET("/api/audit", GetAllAudit) - authorized.GET("/api/audit/:audit_id", GetAudit) - authorized.GET("/api/audit/:audit_id/changes", GetChangeLog) - authorized.GET("/api/audit/:audit_id/changes/:id", GetChangeLogbyId) + authorizedv1 := r.Group("/api/v1") + authorizedv1.Use(auth.AuthenticationMiddleware()) + { + licenses := authorizedv1.Group("/licenses") + { + licenses.POST("", CreateLicense) + licenses.PATCH(":shortname", UpdateLicense) + } + users := authorizedv1.Group("/users") + { + users.GET("", auth.GetAllUser) + users.GET(":id", auth.GetUser) + users.POST("", auth.CreateUser) + } + audit := authorizedv1.Group("/audits") + { + audit.GET("", GetAllAudit) + audit.GET(":audit_id", GetAudit) + audit.GET(":audit_id/changes", GetChangeLogs) + audit.GET(":audit_id/changes/:id", GetChangeLogbyId) + } + obligations := authorizedv1.Group("/obligations") + { + obligations.POST("", CreateObligation) + obligations.PATCH(":topic", UpdateObligation) + obligations.DELETE(":topic", DeleteObligation) + } + } - authorized.POST("/api/obligations", CreateObligation) - authorized.DELETE("/api/obligations/:topic", DeleteObligation) - authorized.PATCH("/api/obligations/:topic", UpdateObligation) - r.GET("/api/obligations", GetAllObligation) - r.GET("/api/obligations/:topic", GetObligation) + // Host the swagger UI at /swagger/index.html + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) return r } @@ -66,7 +114,7 @@ func HandleInvalidUrl(c *gin.Context) { c.JSON(http.StatusNotFound, er) } -// The get all License function returns all the license data present in the database +// GetAllLicense The get all License function returns all the license data present in the database. func GetAllLicense(c *gin.Context) { var licenses []models.LicenseDB @@ -94,9 +142,18 @@ func GetAllLicense(c *gin.Context) { c.JSON(http.StatusOK, res) } -// Get license functions return data of the particular license by its shortname. -// It inputs the shortname as query parameter -// It returns error if no such license exists +// GetLicense to get a single license by its shortname +// +// @Summary Get a license by shortname +// @Description Get a single license by its shortname +// @Id GetLicense +// @Tags Licenses +// @Accept json +// @Produce json +// @Param shortname path string true "Shortname of the license" +// @Success 200 {object} models.LicenseResponse +// @Failure 404 {object} models.LicenseError "License with shortname not found" +// @Router /licenses/{shortname} [get] func GetLicense(c *gin.Context) { var license models.LicenseDB @@ -115,7 +172,7 @@ func GetLicense(c *gin.Context) { Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } @@ -130,8 +187,21 @@ func GetLicense(c *gin.Context) { c.JSON(http.StatusOK, res) } -// The Create License function creates license in the database and add the required data -// It returns the license if it already exists in the database. +// CreateLicense creates a new license in the database. +// +// @Summary Create a new license +// @Description Create a new license in the service +// @Id CreateLicense +// @Tags Licenses +// @Accept json +// @Produce json +// @Param license body models.LicenseInput true "New license to be created" +// @Success 201 {object} models.LicenseResponse "New license created successfully" +// @Failure 400 {object} models.LicenseError "Invalid request body" +// @Failure 409 {object} models.LicenseError "License with same shortname already exists" +// @Failure 500 {object} models.LicenseError "Failed to create license" +// @Security BasicAuth +// @Router /licenses [post] func CreateLicense(c *gin.Context) { var input models.LicenseInput @@ -174,13 +244,13 @@ func CreateLicense(c *gin.Context) { if result.RowsAffected == 0 { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusConflict, Message: "can not create license with same shortname", Error: fmt.Sprintf("Error: License with shortname '%s' already exists", input.Shortname), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusConflict, er) return } if result.Error != nil { @@ -205,9 +275,23 @@ func CreateLicense(c *gin.Context) { c.JSON(http.StatusCreated, res) } -// The Update license functions updates the particular license with a particular shortname. -// It also creates the audit and change logs of the updates -// It returns the updated license +// UpdateLicense Update license with given shortname and create audit and changelog entries. +// +// @Summary Update a license +// @Description Update a license in the service +// @Id UpdateLicense +// @Tags Licenses +// @Accept json +// @Produce json +// @Param shortname path string true "Shortname of the license to be updated" +// @Param license body models.LicenseDB true "Update license body" +// @Success 200 {object} models.LicenseResponse "License updated successfully" +// @Failure 400 {object} models.LicenseError "Invalid license body" +// @Failure 404 {object} models.LicenseError "License with shortname not found" +// @Failure 409 {object} models.LicenseError "License with same shortname already exists" +// @Failure 500 {object} models.LicenseError "Failed to update license" +// @Security BasicAuth +// @Router /licenses/{shortname} [patch] func UpdateLicense(c *gin.Context) { var update models.LicenseDB var license models.LicenseDB @@ -218,13 +302,13 @@ func UpdateLicense(c *gin.Context) { shortname := c.Param("shortname") if err := db.DB.Where(models.LicenseDB{Shortname: shortname}).First(&license).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: fmt.Sprintf("license with shortname '%s' not found", shortname), Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } oldlicense = license @@ -454,8 +538,26 @@ func UpdateLicense(c *gin.Context) { } -// The filter licenses returns the licenses after passing through certain filters. -// It takes the filters as query parameters and filters accordingly. +// FilterLicense Get licenses from service based on different filters. +// +// @Summary Filter licenses +// @Description Filter licenses based on different parameters +// @Id FilterLicense +// @Tags Licenses +// @Accept json +// @Produce json +// @Param spdxid query string false "SPDX ID of the license" +// @Param detector_type query int false "License detector type" +// @Param gplv2compatible query bool false "GPLv2 compatibility flag status of license" +// @Param gplv3compatible query bool false "GPLv3 compatibility flag status of license" +// @Param marydone query bool false "Mary done flag status of license" +// @Param active query bool false "Active license only" +// @Param osiapproved query bool false "OSI Approved flag status of license" +// @Param fsffree query bool false "FSF Free flag status of license" +// @Param copyleft query bool false "Copyleft flag status of license" +// @Success 200 {object} models.LicenseResponse "Filtered licenses" +// @Failure 400 {object} models.LicenseError "Invalid value" +// @Router /licenses [get] func FilterLicense(c *gin.Context) { SpdxId := c.Query("spdxid") DetectorType := c.Query("detector_type") @@ -514,7 +616,7 @@ func FilterLicense(c *gin.Context) { if err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, - Message: "Invalid detector type value", + Message: "invalid detector type value", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), @@ -573,10 +675,19 @@ func FilterLicense(c *gin.Context) { } -// SearchInLicense searches for license data based on user-provided search criteria. -// It accepts a JSON request body containing search parameters and responds with JSON -// containing the matching license data or error messages if the search request is -// invalid or if the search algorithm is not supported. +// SearchInLicense Search for license data based on user-provided search criteria. +// +// @Summary Search licenses +// @Description Search licenses on different filters and algorithms +// @Id SearchInLicense +// @Tags Licenses +// @Accept json +// @Produce json +// @Param search body models.SearchLicense true "Search criteria" +// @Success 200 {object} models.LicenseResponse "Licenses matched" +// @Failure 400 {object} models.LicenseError "Invalid request" +// @Failure 404 {object} models.LicenseError "Search algorithm doesn't exist" +// @Router /search [post] func SearchInLicense(c *gin.Context) { var input models.SearchLicense @@ -599,16 +710,15 @@ func SearchInLicense(c *gin.Context) { query = query.Where(fmt.Sprintf("%s ILIKE ?", input.Field), fmt.Sprintf("%%%s%%", input.SearchTerm)) } else if input.Search == "" || input.Search == "full_text_search" { query = query.Where(input.Field+" @@ plainto_tsquery(?)", input.SearchTerm) - } else { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "search algorithm doesn't exist", Error: "search algorithm with such name doesn't exists", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } query.Find(&license) @@ -624,20 +734,30 @@ func SearchInLicense(c *gin.Context) { } -// GetAllAudit retrieves a list of all audit records from the database and responds with -// JSON containing the audit data or an error message if the records are not found. +// GetAllAudit retrieves a list of all audit records from the database +// +// @Summary Get audit records +// @Description Get all audit records from the server +// @Id GetAllAudit +// @Tags Audits +// @Accept json +// @Produce json +// @Success 200 {object} models.AuditResponse "Audit records" +// @Failure 404 {object} models.LicenseError "Not changelogs in DB" +// @Security BasicAuth +// @Router /audits [get] func GetAllAudit(c *gin.Context) { var audit []models.Audit if err := db.DB.Find(&audit).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "Change log not found", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } res := models.AuditResponse{ @@ -651,25 +771,37 @@ func GetAllAudit(c *gin.Context) { c.JSON(http.StatusOK, res) } -// GetAudit retrieves a specific audit record by its ID from the database and responds -// with JSON containing the audit data or an error message if the record is not found. +// GetAudit retrieves a specific audit record by its ID from the database +// +// @Summary Get an audit record +// @Description Get a specific audit records by ID +// @Id GetAudit +// @Tags Audits +// @Accept json +// @Produce json +// @Param audit_id path string true "Audit ID" +// @Success 200 {object} models.AuditResponse +// @Failure 400 {object} models.LicenseError "Invalid audit ID" +// @Failure 404 {object} models.LicenseError "No audit entry with given ID" +// @Security BasicAuth +// @Router /audits/{audit_id} [get] func GetAudit(c *gin.Context) { var changelog models.Audit id := c.Param("audit_id") - parsedId, err := parseId(c, id, "audit") + parsedId, err := utils.ParseIdToInt(c, id, "audit") if err != nil { return } if err := db.DB.Where(models.Audit{Id: parsedId}).First(&changelog).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "no change log with such id exists", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) } res := models.AuditResponse{ Data: []models.Audit{changelog}, @@ -682,42 +814,37 @@ func GetAudit(c *gin.Context) { c.JSON(http.StatusOK, res) } -func parseId(c *gin.Context, id string, idType string) (int, error) { - parsedId, err := strconv.Atoi(id) - if err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: fmt.Sprintf("invalid %s id", idType), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return 0, err - } - return parsedId, nil -} - -// GetChangeLog retrieves a list of change history records associated with a specific -// audit by its audit ID from the database and responds with JSON containing the change -// history data or an error message if no records are found. -func GetChangeLog(c *gin.Context) { +// GetChangeLogs retrieves a list of change history records associated with a specific audit +// +// @Summary Get changelogs +// @Description Get changelogs of an audit record +// @Id GetChangeLogs +// @Tags Audits +// @Accept json +// @Produce json +// @Param audit_id path string true "Audit ID" +// @Success 200 {object} models.ChangeLogResponse +// @Failure 400 {object} models.LicenseError "Invalid audit ID" +// @Failure 404 {object} models.LicenseError "No audit entry with given ID" +// @Security BasicAuth +// @Router /audits/{audit_id}/changes [get] +func GetChangeLogs(c *gin.Context) { var changelog []models.ChangeLog id := c.Param("audit_id") - parsedId, err := parseId(c, id, "audit") + parsedId, err := utils.ParseIdToInt(c, id, "audit") if err != nil { return } if err := db.DB.Where(models.ChangeLog{AuditId: parsedId}).Find(&changelog).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "no change log with such id exists", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) } res := models.ChangeLogResponse{ @@ -732,40 +859,52 @@ func GetChangeLog(c *gin.Context) { } // GetChangeLogbyId retrieves a specific change history record by its ID for a given audit. -// It responds with JSON containing the change history data or error messages if the record -// is not found or if it does not belong to the specified audit. +// +// @Summary Get a changelog +// @Description Get a specific changelog of an audit record by its ID +// @Id GetChangeLogbyId +// @Tags Audits +// @Accept json +// @Produce json +// @Param audit_id path string true "Audit ID" +// @Param id path string true "Changelog ID" +// @Success 200 {object} models.ChangeLogResponse +// @Failure 400 {object} models.LicenseError "Invalid ID" +// @Failure 404 {object} models.LicenseError "No changelog with given ID found" +// @Security BasicAuth +// @Router /audits/{audit_id}/changes/{id} [get] func GetChangeLogbyId(c *gin.Context) { var changelog models.ChangeLog auditId := c.Param("audit_id") - parsedAuditId, err := parseId(c, auditId, "audit") + parsedAuditId, err := utils.ParseIdToInt(c, auditId, "audit") if err != nil { return } changelogId := c.Param("id") - parsedChangeLogId, err := parseId(c, changelogId, "change log") + parsedChangeLogId, err := utils.ParseIdToInt(c, changelogId, "changelog") if err != nil { return } if err := db.DB.Where(models.ChangeLog{Id: parsedChangeLogId}).Find(&changelog).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "no change history with such id exists", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) } if changelog.AuditId != parsedAuditId { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "no change history with such id and audit id exists", Error: "Invalid change history for the requested audit id", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) } res := models.ChangeLogResponse{ Data: []models.ChangeLog{changelog}, @@ -777,10 +916,21 @@ func GetChangeLogbyId(c *gin.Context) { c.JSON(http.StatusOK, res) } -// CreateObligation creates a new obligation record based on the provided input JSON data. -// It performs validation, generates an MD5 hash of the obligation text, and associates -// the obligation with relevant licenses. The function responds with JSON containing the -// newly created obligation data or error messages in case of validation or database errors. +// CreateObligation creates a new obligation record and associates it with relevant licenses. +// +// @Summary Create an obligation +// @Description Create an obligation and associate it with licenses +// @Id CreateObligation +// @Tags Obligations +// @Accept json +// @Produce json +// @Param obligation body models.ObligationInput true "Obligation to create" +// @Success 201 {object} models.ObligationResponse +// @Failure 400 {object} models.LicenseError "Bad request body" +// @Failure 409 {object} models.LicenseError "Obligation with same body exists" +// @Failure 500 {object} models.LicenseError "Unable to create obligation" +// @Security BasicAuth +// @Router /obligations [post] func CreateObligation(c *gin.Context) { var input models.ObligationInput @@ -798,7 +948,6 @@ func CreateObligation(c *gin.Context) { s := input.Text hash := md5.Sum([]byte(s)) md5hash := hex.EncodeToString(hash[:]) - fmt.Printf(md5hash) input.Active = true input.TextUpdatable = false @@ -814,7 +963,6 @@ func CreateObligation(c *gin.Context) { TextUpdatable: input.TextUpdatable, Active: input.Active, } - fmt.Print(obligation) result := db.DB.Debug().FirstOrCreate(&obligation) @@ -822,13 +970,13 @@ func CreateObligation(c *gin.Context) { if result.RowsAffected == 0 { er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "can not create obligation with same Md5", - Error: fmt.Sprintf("Error: Obligation with Md5 '%s' already exists", obligation.Md5), + Status: http.StatusConflict, + Message: "can not create obligation with same MD5", + Error: fmt.Sprintf("Error: Obligation with MD5 '%s' already exists", obligation.Md5), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusConflict, er) return } if result.Error != nil { @@ -863,8 +1011,17 @@ func CreateObligation(c *gin.Context) { c.JSON(http.StatusCreated, res) } -// GetAllObligation retrieves a list of all active obligation records from the database and -// responds with JSON containing the obligation data or an error message if no records are found. +// GetAllObligation retrieves a list of all active obligation records +// +// @Summary Get all active obligations +// @Description Get all active obligations from the service +// @Id GetAllObligation +// @Tags Obligations +// @Accept json +// @Produce json +// @Success 200 {object} models.ObligationResponse +// @Failure 404 {object} models.LicenseError "No obligations in DB" +// @Router /obligations [get] func GetAllObligation(c *gin.Context) { var obligations []models.Obligation query := db.DB.Model(&obligations) @@ -872,13 +1029,13 @@ func GetAllObligation(c *gin.Context) { err := query.Find(&obligations).Error if err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "Obligations not found", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } res := models.ObligationResponse{ @@ -892,10 +1049,22 @@ func GetAllObligation(c *gin.Context) { c.JSON(http.StatusOK, res) } -// UpdateObligation updates an existing active obligation record based on the provided input JSON data. -// It performs validation, updates the specified fields, and records changes in the audit log. -// The function responds with JSON containing the updated obligation data or error messages in case -// of validation or database errors. +// UpdateObligation updates an existing active obligation record +// +// @Summary Update obligation +// @Description Update an existing obligation record +// @Id UpdateObligation +// @Tags Obligations +// @Accept json +// @Produce json +// @Param topic path string true "Topic of the obligation to be updated" +// @Param obligation body models.UpdateObligation true "Obligation to be updated" +// @Success 200 {object} models.ObligationResponse +// @Failure 400 {object} models.LicenseError "Invalid request" +// @Failure 404 {object} models.LicenseError "No obligation with given topic found" +// @Failure 500 {object} models.LicenseError "Unable to update obligation" +// @Security BasicAuth +// @Router /obligations/{topic} [patch] func UpdateObligation(c *gin.Context) { var update models.UpdateObligation var oldobligation models.Obligation @@ -906,13 +1075,13 @@ func UpdateObligation(c *gin.Context) { tp := c.Param("topic") if err := query.Where(models.Obligation{Active: true, Topic: tp}).First(&obligation).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", tp), Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } oldobligation = obligation @@ -1001,8 +1170,8 @@ func UpdateObligation(c *gin.Context) { change := models.ChangeLog{ AuditId: audit.Id, Field: "Modification", - OldValue: oldobligation.Modifications, - UpdatedValue: obligation.Modifications, + OldValue: strconv.FormatBool(oldobligation.Modifications), + UpdatedValue: strconv.FormatBool(obligation.Modifications), } db.DB.Create(&change) } @@ -1052,41 +1221,63 @@ func UpdateObligation(c *gin.Context) { c.JSON(http.StatusOK, res) } -// DeleteObligation marks an existing obligation record as inactive based on the provided topic parameter. -// It responds with an error message if the obligation is not found or if the deactivation operation fails. +// DeleteObligation marks an existing obligation record as inactive +// +// @Summary Deactivate obligation +// @Description Deactivate an obligation +// @Id DeleteObligation +// @Tags Obligations +// @Accept json +// @Produce json +// @Param topic path string true "Topic of the obligation to be updated" +// @Success 204 +// @Failure 404 {object} models.LicenseError "No obligation with given topic found" +// @Security BasicAuth +// @Router /obligations/{topic} [delete] func DeleteObligation(c *gin.Context) { var obligation models.Obligation tp := c.Param("topic") if err := db.DB.Where(models.Obligation{Topic: tp}).First(&obligation).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", tp), Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } obligation.Active = false + db.DB.Where(models.Obligation{Topic: tp}).Save(&obligation) + c.Status(http.StatusNoContent) } -// GetObligation retrieves an active obligation record based on the provided topic parameter. -// It responds with JSON containing the obligation data or an error message if the obligation -// is not found or if there is an error during retrieval. +// GetObligation retrieves an active obligation record +// +// @Summary Get an obligation +// @Description Get an active based on given topic +// @Id GetObligation +// @Tags Obligations +// @Accept json +// @Produce json +// @Param topic path string true "Topic of the obligation" +// @Success 200 {object} models.ObligationResponse +// @Failure 404 {object} models.LicenseError "No obligation with given topic found" +// @Router /obligations/{topic} [get] func GetObligation(c *gin.Context) { var obligation models.Obligation query := db.DB.Model(&obligation) tp := c.Param("topic") if err := query.Where(models.Obligation{Active: true, Topic: tp}).First(&obligation).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: fmt.Sprintf("obligation with topic '%s' not found", tp), Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } res := models.ObligationResponse{ diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 8a61c0a..fab70dc 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -7,16 +7,16 @@ import ( "bytes" "encoding/base64" "encoding/json" - "net/http" "net/http/httptest" "os" "testing" - "github.com/fossology/LicenseDb/pkg/db" - "github.com/fossology/LicenseDb/pkg/models" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + + "github.com/fossology/LicenseDb/pkg/db" + "github.com/fossology/LicenseDb/pkg/models" ) // TestMain is the main testing function for the application. It sets up the testing environment, diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 563f4f5..3cdb915 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -1,4 +1,7 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla +// SPDX-FileCopyrightText: 2023 Siemens AG +// SPDX-FileContributor: Gaurav Mishra +// // SPDX-License-Identifier: GPL-2.0-only package auth @@ -10,12 +13,26 @@ import ( "strings" "time" + "github.com/gin-gonic/gin" + "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" - "github.com/gin-gonic/gin" + "github.com/fossology/LicenseDb/pkg/utils" ) -// CreateUser creates a new user based on the provided JSON request data. +// CreateUser creates a new user +// +// @Summary Create new user +// @Description Create a new service user +// @Id CreateUser +// @Tags Users +// @Accept json +// @Produce json +// @Success 201 {object} models.UserResponse +// @Failure 400 {object} models.LicenseError "Invalid json body" +// @Failure 409 {object} models.LicenseError "User already exists" +// @Security BasicAuth +// @Router /users [post] func CreateUser(c *gin.Context) { var user models.User if err := c.ShouldBindJSON(&user); err != nil { @@ -32,13 +49,13 @@ func CreateUser(c *gin.Context) { result := db.DB.FirstOrCreate(&user) if result.RowsAffected == 0 { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusConflict, Message: "can not create user with same userid", Error: fmt.Sprintf("Error: License with userid '%d' already exists", user.Id), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusConflict, er) return } @@ -54,18 +71,30 @@ func CreateUser(c *gin.Context) { } // GetAllUser retrieves a list of all users from the database. +// +// @Summary Get users +// @Description Get all service users +// @Id GetAllUsers +// @Tags Users +// @Accept json +// @Produce json +// @Success 200 {object} models.UserResponse +// @Failure 404 {object} models.LicenseError "Users not found" +// @Security BasicAuth +// @Router /users [get] func GetAllUser(c *gin.Context) { var users []models.User if err := db.DB.Find(&users).Error; err != nil { er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "can not create user", + Status: http.StatusNotFound, + Message: "Users not found", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusInternalServerError, er) + c.JSON(http.StatusNotFound, er) + return } res := models.UserResponse{ Data: users, @@ -79,19 +108,37 @@ func GetAllUser(c *gin.Context) { } // GetUser retrieves a user by their user ID from the database. +// +// @Summary Get a user +// @Description Get a single user by ID +// @Id GetUser +// @Tags Users +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} models.UserResponse +// @Failure 400 {object} models.LicenseError "Invalid user id" +// @Failure 404 {object} models.LicenseError "User not found" +// @Security BasicAuth +// @Router /users/{id} [get] func GetUser(c *gin.Context) { var user models.User id := c.Param("id") + parsedId, err := utils.ParseIdToInt(c, id, "user") + if err != nil { + return + } - if err := db.DB.Where("id = ?", id).First(&user).Error; err != nil { + if err := db.DB.Where(models.User{Id: parsedId}).First(&user).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusNotFound, Message: "no user with such user id exists", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) + return } res := models.UserResponse{ Data: []models.User{user}, @@ -117,7 +164,7 @@ func AuthenticationMiddleware() gin.HandlerFunc { Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusUnauthorized, er) + c.JSON(http.StatusBadRequest, er) c.Abort() return } @@ -125,14 +172,14 @@ func AuthenticationMiddleware() gin.HandlerFunc { decodedAuth, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic ")) if err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, + Status: http.StatusUnauthorized, Message: "Please check your credentials and try again", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusUnauthorized, er) c.Abort() return } @@ -147,7 +194,7 @@ func AuthenticationMiddleware() gin.HandlerFunc { password := auth[1] var user models.User - result := db.DB.Where("username = ?", username).First(&user) + result := db.DB.Where(models.User{Username: username}).First(&user) if result.Error != nil { er := models.LicenseError{ Status: http.StatusUnauthorized, diff --git a/pkg/db/db.go b/pkg/db/db.go index 8b58f2a..ea131d4 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -9,10 +9,11 @@ import ( "log" "os" - "github.com/fossology/LicenseDb/pkg/models" - "github.com/fossology/LicenseDb/pkg/utils" "gorm.io/driver/postgres" "gorm.io/gorm" + + "github.com/fossology/LicenseDb/pkg/models" + "github.com/fossology/LicenseDb/pkg/utils" ) // DB is a global variable to store the GORM database connection. diff --git a/pkg/models/types.go b/pkg/models/types.go index 32a344a..e928c80 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -1,4 +1,7 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla +// SPDX-FileCopyrightText: 2023 Siemens AG +// SPDX-FileContributor: Gaurav Mishra +// // SPDX-License-Identifier: GPL-2.0-only package models @@ -9,26 +12,26 @@ import "time" // properties associated with it. // It provides structured storage for license-related information. type LicenseDB struct { - Id int64 `json:"rf_id" gorm:"primary_key"` - Shortname string `json:"rf_shortname" gorm:"unique;not null" binding:"required"` - Fullname string `json:"rf_fullname"` - Text string `json:"rf_text"` - Url string `json:"rf_url"` - AddDate time.Time `json:"rf_add_date" gorm:"default:CURRENT_TIMESTAMP"` + Id int64 `json:"rf_id" gorm:"primary_key" example:"123"` + Shortname string `json:"rf_shortname" gorm:"unique;not null" binding:"required" example:"MIT"` + Fullname string `json:"rf_fullname" example:"MIT License"` + Text string `json:"rf_text" example:"MIT License Text here"` + Url string `json:"rf_url" example:"https://opensource.org/licenses/MIT"` + AddDate time.Time `json:"rf_add_date" gorm:"default:CURRENT_TIMESTAMP" example:"2023-12-01T18:10:25.00+05:30"` Copyleft bool `json:"rf_copyleft"` FSFfree bool `json:"rf_FSFfree"` OSIapproved bool `json:"rf_OSIapproved"` GPLv2compatible bool `json:"rf_GPLv2compatible"` GPLv3compatible bool `json:"rf_GPLv3compatible"` - Notes string `json:"rf_notes"` + Notes string `json:"rf_notes" example:"This license has been superseded."` Fedora string `json:"rf_Fedora"` TextUpdatable bool `json:"rf_text_updatable"` - DetectorType int64 `json:"rf_detector_type"` + DetectorType int64 `json:"rf_detector_type" example:"1"` Active bool `json:"rf_active"` Source string `json:"rf_source"` - SpdxId string `json:"rf_spdx_id"` + SpdxId string `json:"rf_spdx_id" example:"MIT"` Risk int64 `json:"rf_risk"` - Flag int64 `json:"rf_flag" gorm:"default:1"` + Flag int64 `json:"rf_flag" gorm:"default:1" example:"1"` Marydone bool `json:"marydone"` } @@ -60,9 +63,9 @@ type LicenseJson struct { // It contains information that provides context and supplementary details // about the retrieved license data. type PaginationMeta struct { - ResourceCount int `json:"resource_count"` - Page int `json:"page,omitempty"` - PerPage int `json:"per_page,omitempty"` + ResourceCount int `json:"resource_count" example:"20"` + Page int `json:"page,omitempty" example:"1"` + PerPage int `json:"per_page,omitempty" example:"10"` } // LicenseResponse struct is representation of design API response of license. @@ -70,7 +73,7 @@ type PaginationMeta struct { // retrieving license information. // It is used to encapsulate license-related data in an organized manner. type LicenseResponse struct { - Status int `json:"status"` + Status int `json:"status" example:"200"` Data []LicenseDB `json:"data"` Meta PaginationMeta `json:"paginationmeta"` } @@ -79,133 +82,132 @@ type LicenseResponse struct { // It provides information about the encountered error, including details such as // status, error message, error type, path, and timestamp. type LicenseError struct { - Status int `json:"status"` - Message string `json:"message"` - Error string `json:"error"` - Path string `json:"path"` - Timestamp string `json:"timestamp"` + Status int `json:"status" example:"400"` + Message string `json:"message" example:"invalid request body"` + Error string `json:"error" example:"invalid request body"` + Path string `json:"path" example:"/api/v1/licenses"` + Timestamp string `json:"timestamp" example:"2023-12-01T10:00:51+05:30"` } // The LicenseInput struct represents the input or payload required for creating a license. // It contains various fields that capture the necessary information for defining a license entity. type LicenseInput struct { - Shortname string `json:"rf_shortname"` - Fullname string `json:"rf_fullname"` - Text string `json:"rf_text"` - Url string `json:"rf_url"` - AddDate time.Time `json:"rf_add_date"` + Shortname string `json:"rf_shortname" example:"MIT"` + Fullname string `json:"rf_fullname" example:"MIT License"` + Text string `json:"rf_text" example:"MIT License Text here"` + Url string `json:"rf_url" example:"https://opensource.org/licenses/MIT"` + AddDate time.Time `json:"rf_add_date" example:"2023-12-01T18:10:25.00+05:30"` Copyleft bool `json:"rf_copyleft"` FSFfree bool `json:"rf_FSFfree"` OSIapproved bool `json:"rf_OSIapproved"` GPLv2compatible bool `json:"rf_GPLv2compatible"` GPLv3compatible bool `json:"rf_GPLv3compatible"` - Notes string `json:"rf_notes"` + Notes string `json:"rf_notes" example:"This license has been superseded."` Fedora string `json:"rf_Fedora"` TextUpdatable bool `json:"rf_text_updatable"` - DetectorType int64 `json:"rf_detector_type"` + DetectorType int64 `json:"rf_detector_type" example:"1"` Active bool `json:"rf_active"` Source string `json:"rf_source"` - SpdxId string `json:"rf_spdx_id"` + SpdxId string `json:"rf_spdx_id" example:"MIT"` Risk int64 `json:"rf_risk"` - Flag int64 `json:"rf_flag"` + Flag int64 `json:"rf_flag" example:"1"` Marydone bool `json:"marydone"` } // User struct is representation of user information. type User struct { - Id int64 `json:"id" gorm:"primary_key"` - Username string `json:"username" gorm:"unique;not null" binding:"required"` - Userlevel string `json:"userlevel" binding:"required"` + Id int64 `json:"id" gorm:"primary_key" example:"123"` + Username string `json:"username" gorm:"unique;not null" binding:"required" example:"fossy"` + Userlevel string `json:"userlevel" binding:"required" example:"admin"` Userpassword string `json:"userpassword" binding:"required"` } // UserResponse struct is representation of design API response of user. type UserResponse struct { - Status int `json:"status"` + Status int `json:"status" example:"200"` Data []User `json:"data"` Meta PaginationMeta `json:"paginationmeta"` } // SearchLicense struct represents the input needed to search in a license. type SearchLicense struct { - Field string `json:"field" binding:"required"` - SearchTerm string `json:"search_term" binding:"required"` - Search string `json:"search"` + Field string `json:"field" binding:"required" example:"rf_text"` + SearchTerm string `json:"search_term" binding:"required" example:"MIT License"` + Search string `json:"search" enums:"fuzzy,full_text_search"` } // Audit struct represents an audit entity with certain attributes and properties // It has user id as a foreign key type Audit struct { - Id int `json:"id" gorm:"primary_key"` - UserId int64 `json:"user_id"` + Id int64 `json:"id" gorm:"primary_key" example:"456"` + UserId int64 `json:"user_id" example:"123"` User User `gorm:"foreignKey:UserId;references:Id" json:"-"` - TypeId int64 `json:"type_id"` - Timestamp time.Time `json:"timestamp"` - Type string `json:"type"` + TypeId int64 `json:"type_id" example:"34"` + Timestamp time.Time `json:"timestamp" example:"2023-12-01T18:10:25.00+05:30"` + Type string `json:"type" example:"license"` } // ChangeLog struct represents a change entity with certain attributes and properties type ChangeLog struct { - Id int `json:"id" gorm:"primary_key"` - Field string `json:"field"` - UpdatedValue string `json:"updated_value"` - OldValue string `json:"old_value"` - AuditId int `json:"audit_id"` + Id int64 `json:"id" gorm:"primary_key" example:"789"` + Field string `json:"field" example:"rf_text"` + UpdatedValue string `json:"updated_value" example:"New license text"` + OldValue string `json:"old_value" example:"Old license text"` + AuditId int64 `json:"audit_id" example:"456"` Audit Audit `gorm:"foreignKey:AuditId;references:Id" json:"-"` } // ChangeLogResponse represents the design of API response of change log type ChangeLogResponse struct { - Status int `json:"status"` + Status int `json:"status" example:"200"` Data []ChangeLog `json:"data"` Meta PaginationMeta `json:"paginationmeta"` } // AuditResponse represents the response format for audit data. type AuditResponse struct { - Status int `json:"status"` + Status int `json:"status" example:"200"` Data []Audit `json:"data"` Meta PaginationMeta `json:"paginationmeta"` } // Obligation represents an obligation record in the database. type Obligation struct { - Id int64 `json:"id" gorm:"primary_key"` - Topic string `json:"topic"` - Type string `json:"type"` - Text string `json:"text"` - Classification string `json:"classification"` - Modifications string `json:"modifications"` - Comment string `json:"comment"` + Id int64 `json:"id" gorm:"primary_key" example:"147"` + Topic string `json:"topic" example:"copyleft"` + Type string `json:"type" enums:"obligation,restriction,risk,right"` + Text string `json:"text" example:"Source code be made available when distributing the software."` + Classification string `json:"classification" enums:"green,white,yellow,red"` + Modifications bool `json:"modifications"` + Comment string `json:"comment" example:"This is a comment."` Active bool `json:"active"` TextUpdatable bool `json:"text_updatable"` - Md5 string `json:"md5" gorm:"unique"` + Md5 string `json:"md5" gorm:"unique" example:"deadbeef"` } // ObligationInput represents the input format for creating a new obligation. type ObligationInput struct { - Topic string `json:"topic" binding:"required"` - Type string `json:"type" binding:"required"` - Text string `json:"text" binding:"required"` - Classification string `json:"classification"` - Modifications string `json:"modifications"` - Comment string `json:"comment"` - Active bool `json:"active"` + Topic string `json:"topic" binding:"required" example:"copyleft"` + Type string `json:"type" binding:"required" enums:"obligation,restriction,risk,right"` + Text string `json:"text" binding:"required" example:"Source code be made available when distributing the software."` + Classification string `json:"classification" enums:"green,white,yellow,red"` + Modifications bool `json:"modifications"` + Comment string `json:"comment" example:"This is a comment."` + Active bool `json:"active" example:"true"` TextUpdatable bool `json:"text_updatable"` - Shortnames []string `json:"shortnames"` + Shortnames []string `json:"shortnames" example:"GPL-2.0-only,GPL-2.0-or-later"` } // UpdateObligation represents the input format for updating an existing obligation. type UpdateObligation struct { - Topic string `json:"topic"` - Type string `json:"type"` - Text string `json:"text"` - Classification string `json:"classification"` - Modifications string `json:"modifications"` - Comment string `json:"comment"` - Active bool `json:"active"` + Topic string `json:"topic" binding:"required" example:"copyleft"` + Type string `json:"type" binding:"required" enums:"obligation,restriction,risk,right"` + Text string `json:"text" binding:"required" example:"Source code be made available when distributing the software."` + Classification string `json:"classification" enums:"green,white,yellow,red"` + Modifications bool `json:"modifications"` + Comment string `json:"comment" example:"This is a comment."` + Active bool `json:"active" example:"true"` TextUpdatable bool `json:"text_updatable"` - Md5 string `json:"md5"` } // ObligationMap represents the mapping between an obligation and a license. @@ -219,7 +221,7 @@ type ObligationMap struct { // ObligationResponse represents the response format for obligation data. type ObligationResponse struct { - Status int `json:"status"` + Status int `json:"status" example:"200"` Data []Obligation `json:"data"` Meta PaginationMeta `json:"paginationmeta"` } diff --git a/pkg/utils/util.go b/pkg/utils/util.go index 8cfc57c..14d5022 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -1,12 +1,20 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla +// SPDX-FileCopyrightText: 2023 Siemens AG +// SPDX-FileContributor: Gaurav Mishra +// // SPDX-License-Identifier: GPL-2.0-only package utils import ( - "github.com/fossology/LicenseDb/pkg/models" + "fmt" + "net/http" "strconv" "time" + + "github.com/gin-gonic/gin" + + "github.com/fossology/LicenseDb/pkg/models" ) // The Converter function takes an input of type models.LicenseJson and converts it into a @@ -95,3 +103,21 @@ func Converter(input models.LicenseJson) models.LicenseDB { } return result } + +// ParseIdToInt convert the string ID from gin.Context to an integer and throw error if conversion fails. Also, +// update the gin.Context with REST API error. +func ParseIdToInt(c *gin.Context, id string, idType string) (int64, error) { + parsedId, err := strconv.ParseInt(id, 10, 64) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: fmt.Sprintf("invalid %s id", idType), + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return 0, err + } + return parsedId, nil +}