diff --git a/.gitea/workflows/ci-back.yml b/.gitea/workflows/ci-back.yml index e2db25c..effb49f 100644 --- a/.gitea/workflows/ci-back.yml +++ b/.gitea/workflows/ci-back.yml @@ -1,10 +1,6 @@ name: Backend ci on: - push: - branches: - - master - - develop pull_request: branches: - master diff --git a/backend/?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON b/backend/?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON deleted file mode 100644 index 2da5919..0000000 Binary files a/backend/?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON and /dev/null differ diff --git a/backend/Dockerfile b/backend/Dockerfile index 8f70254..fc66c3b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -15,5 +15,6 @@ FROM alpine:3.23.0 COPY --from=builder /app/backend . EXPOSE 8080 +RUN apk add --no-cache curl HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit 1 CMD ["./backend"] diff --git a/backend/Makefile b/backend/Makefile index bd98eea..10bad81 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,4 +1,4 @@ -.PHONY: test build clean lint dev +.PHONY: test build clean lint dev run-docker docs-upd docker test: @@ -19,3 +19,16 @@ lint: dev: swag init -g ./cmd/main.go --parseDependency --parseInternal go run ./cmd/main.go + + +docs-upd: + swag init -g ./cmd/main.go --parseDependency --parseInternal + + +docker: + swag init -g ./cmd/main.go --parseDependency --parseInternal + docker build -t backend . + +run-docker: + docker build -t backend . + docker run --rm -p 8080:8080 --env-file .env backend:latest diff --git a/backend/cmd/main.go b/backend/cmd/main.go index bcde071..2f91c67 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -10,6 +10,10 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization +// @description Type "Bearer" followed by a space and the JWT token. func main() { log := logger.New(false) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..c0e96a0 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,12 @@ +services: + backend: + image: backend:latest + env_file: + - .env + ports: + - 8080:8080 + volumes: + - db-data:/var/lib/backend/data + +volumes: + db-data: diff --git a/backend/docs/docs.go b/backend/docs/docs.go index fb0a6ec..70aaa57 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -15,22 +15,58 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/auth/github": { + "get": { + "description": "Redirects to GitHub authorization", + "tags": [ + "auth" + ], + "summary": "Start GitHub OAuth login", + "responses": { + "302": { + "description": "Found" + } + } + } + }, "/callback/github": { "get": { - "description": "Callback for oauth2 providers", - "consumes": [ - "application/json" - ], + "description": "Exchanges authorization code for access token", "produces": [ "application/json" ], "tags": [ "auth" ], - "summary": "Callback for oauth2 providers", + "summary": "GitHub OAuth callback", + "parameters": [ + { + "type": "string", + "description": "Authorization code", + "name": "code", + "in": "query", + "required": true + } + ], "responses": { "200": { - "description": "OK", + "description": "Access token", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Missing code", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Exchange failed", "schema": { "type": "object", "additionalProperties": { @@ -58,15 +94,50 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" - } + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "404": { + "description": "No Post found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } }, "post": { + "security": [ + { + "Bearer": [] + } + ], "description": "Create new post", "consumes": [ "application/json" @@ -93,13 +164,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + ] } }, "400": { - "description": "Bad Request", + "description": "Invalid request", "schema": { - "$ref": "#/definitions/gin.H" + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } @@ -131,30 +214,47 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + ] } }, "400": { "description": "Invalid ID format", "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Post not found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } }, "put": { + "security": [ + { + "Bearer": [] + } + ], "description": "Update post", "consumes": [ "application/json" @@ -188,12 +288,41 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" + } + } + } + ] + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } }, "delete": { + "security": [ + { + "Bearer": [] + } + ], "description": "Delete post", "consumes": [ "application/json" @@ -205,11 +334,80 @@ const docTemplate = `{ "posts" ], "summary": "Delete post", + "parameters": [ + { + "type": "integer", + "description": "Post ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + ] + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Post not found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + } + } + } + }, + "/session": { + "get": { + "description": "Returns user session data", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get user session", + "responses": { + "200": { + "description": "Session data", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -217,9 +415,33 @@ const docTemplate = `{ } }, "definitions": { - "gin.H": { + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail": { "type": "object", - "additionalProperties": {} + "properties": { + "code": { + "type": "integer" + }, + "detail": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse": { + "type": "object", + "properties": { + "data": {} + } }, "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": { "type": "object", @@ -227,7 +449,7 @@ const docTemplate = `{ "content": { "type": "string" }, - "createdAt": { + "created_at": { "type": "string" }, "id": { @@ -263,6 +485,14 @@ const docTemplate = `{ } } } + }, + "securityDefinitions": { + "Bearer": { + "description": "Type \"Bearer\" followed by a space and the JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index c52c036..86967b0 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -4,22 +4,58 @@ "contact": {} }, "paths": { + "/auth/github": { + "get": { + "description": "Redirects to GitHub authorization", + "tags": [ + "auth" + ], + "summary": "Start GitHub OAuth login", + "responses": { + "302": { + "description": "Found" + } + } + } + }, "/callback/github": { "get": { - "description": "Callback for oauth2 providers", - "consumes": [ - "application/json" - ], + "description": "Exchanges authorization code for access token", "produces": [ "application/json" ], "tags": [ "auth" ], - "summary": "Callback for oauth2 providers", + "summary": "GitHub OAuth callback", + "parameters": [ + { + "type": "string", + "description": "Authorization code", + "name": "code", + "in": "query", + "required": true + } + ], "responses": { "200": { - "description": "OK", + "description": "Access token", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Missing code", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Exchange failed", "schema": { "type": "object", "additionalProperties": { @@ -47,15 +83,50 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" - } + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "404": { + "description": "No Post found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } }, "post": { + "security": [ + { + "Bearer": [] + } + ], "description": "Create new post", "consumes": [ "application/json" @@ -82,13 +153,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + ] } }, "400": { - "description": "Bad Request", + "description": "Invalid request", "schema": { - "$ref": "#/definitions/gin.H" + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } @@ -120,30 +203,47 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" + } + } + } + ] } }, "400": { "description": "Invalid ID format", "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Post not found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } }, "500": { "description": "Internal server error", "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } }, "put": { + "security": [ + { + "Bearer": [] + } + ], "description": "Update post", "consumes": [ "application/json" @@ -177,12 +277,41 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" + } + } + } + ] + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" } } } }, "delete": { + "security": [ + { + "Bearer": [] + } + ], "description": "Delete post", "consumes": [ "application/json" @@ -194,11 +323,80 @@ "posts" ], "summary": "Delete post", + "parameters": [ + { + "type": "integer", + "description": "Post ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + "allOf": [ + { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" + } + } + } + ] + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Post not found", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse" + } + } + } + } + }, + "/session": { + "get": { + "description": "Returns user session data", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get user session", + "responses": { + "200": { + "description": "Session data", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -206,9 +404,33 @@ } }, "definitions": { - "gin.H": { + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail": { "type": "object", - "additionalProperties": {} + "properties": { + "code": { + "type": "integer" + }, + "detail": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse": { + "type": "object", + "properties": { + "data": {} + } }, "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": { "type": "object", @@ -216,7 +438,7 @@ "content": { "type": "string" }, - "createdAt": { + "created_at": { "type": "string" }, "id": { @@ -252,5 +474,13 @@ } } } + }, + "securityDefinitions": { + "Bearer": { + "description": "Type \"Bearer\" followed by a space and the JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 3a1cd46..6bbafa1 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,12 +1,27 @@ definitions: - gin.H: - additionalProperties: {} + gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail: + properties: + code: + type: integer + detail: + type: string + message: + type: string + type: object + gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse: + properties: + error: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail' + type: object + gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse: + properties: + data: {} type: object gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post: properties: content: type: string - createdAt: + created_at: type: string id: type: integer @@ -32,21 +47,45 @@ definitions: info: contact: {} paths: + /auth/github: + get: + description: Redirects to GitHub authorization + responses: + "302": + description: Found + summary: Start GitHub OAuth login + tags: + - auth /callback/github: get: - consumes: - - application/json - description: Callback for oauth2 providers + description: Exchanges authorization code for access token + parameters: + - description: Authorization code + in: query + name: code + required: true + type: string produces: - application/json responses: "200": - description: OK + description: Access token + schema: + additionalProperties: true + type: object + "400": + description: Missing code schema: additionalProperties: type: string type: object - summary: Callback for oauth2 providers + "500": + description: Exchange failed + schema: + additionalProperties: + type: string + type: object + summary: GitHub OAuth callback tags: - auth /posts: @@ -60,9 +99,26 @@ paths: "200": description: OK schema: - items: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' - type: array + allOf: + - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + - properties: + data: + items: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + type: array + type: object + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "404": + description: No Post found + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' summary: Get all posts tags: - posts @@ -83,11 +139,18 @@ paths: "200": description: OK schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + allOf: + - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + - properties: + data: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post' + type: object "400": - description: Bad Request + description: Invalid request schema: - $ref: '#/definitions/gin.H' + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + security: + - Bearer: [] summary: Create post tags: - posts @@ -96,13 +159,38 @@ paths: consumes: - application/json description: Delete post + parameters: + - description: Post ID + in: path + name: id + required: true + type: integer produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post' + allOf: + - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + - properties: + data: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post' + type: object + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "404": + description: Post not found + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + security: + - Bearer: [] summary: Delete post tags: - posts @@ -122,19 +210,24 @@ paths: "200": description: OK schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + allOf: + - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + - properties: + data: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' + type: object "400": description: Invalid ID format schema: - additionalProperties: - type: string - type: object + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "404": + description: Post not found + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' "500": description: Internal server error schema: - additionalProperties: - type: string - type: object + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' summary: Get post by id tags: - posts @@ -160,8 +253,49 @@ paths: "200": description: OK schema: - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate' + allOf: + - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse' + - properties: + data: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate' + type: object + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse' + security: + - Bearer: [] summary: Update post tags: - posts + /session: + get: + description: Returns user session data + produces: + - application/json + responses: + "200": + description: Session data + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Get user session + tags: + - auth +securityDefinitions: + Bearer: + description: Type "Bearer" followed by a space and the JWT token. + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/backend/go.mod b/backend/go.mod index 8341c4a..b5a06b0 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,23 +2,29 @@ module gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend go 1.25.6 +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 + golang.org/x/oauth2 v0.35.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + modernc.org/sqlite v1.44.3 +) + require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.2.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.0 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/spec v0.22.3 // indirect - github.com/go-openapi/swag v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect @@ -31,13 +37,11 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -46,9 +50,6 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/swaggo/files v1.0.1 // indirect - github.com/swaggo/gin-swagger v1.6.1 // indirect - github.com/swaggo/swag v1.16.6 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.uber.org/mock v0.6.0 // indirect @@ -58,17 +59,12 @@ require ( golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/tools v0.41.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.44.3 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 3a6e0ba..544e0c2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,30 +1,23 @@ 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.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= -github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= -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/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -35,14 +28,15 @@ github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmG github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= -github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= -github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= @@ -51,6 +45,12 @@ github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv4 github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -63,21 +63,25 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -89,6 +93,7 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= @@ -96,17 +101,19 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= @@ -122,7 +129,6 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -173,20 +179,38 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go index e836759..7a5fab5 100644 --- a/backend/internal/auth/jwt.go +++ b/backend/internal/auth/jwt.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "time" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" "github.com/gin-gonic/gin" @@ -14,9 +15,13 @@ var jwtSecret = []byte(os.Getenv("JWT_SECRET")) func GenerateJWT(user storage.User) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ - "id": user.ID, - "email": user.Email, - "github_id": user.GithubID, + "id": user.ID, + "email": user.Email, + "login": user.GithubLogin, + "github_id": user.GithubID, + "avatar_url": user.AvatarURL, + "exp": time.Now().Add(30 * 24 * time.Hour).Unix(), // 30 дней + "iat": time.Now().Unix(), }) tokenString, err := token.SignedString(jwtSecret) if err != nil { @@ -35,7 +40,6 @@ func JWTMiddleware() gin.HandlerFunc { tokenString := strings.TrimPrefix(auth, "Bearer ") token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } @@ -53,8 +57,118 @@ func JWTMiddleware() gin.HandlerFunc { return } - c.Set("user_id", int(claims["id"].(float64))) - c.Set("login", claims["login"].(string)) + idValue, idExists := claims["id"] + if !idExists { + c.AbortWithStatusJSON(401, gin.H{"error": "missing id in token"}) + return + } + + idFloat, ok := idValue.(float64) + if !ok { + c.AbortWithStatusJSON(401, gin.H{"error": "invalid id type in token"}) + return + } + + githubIDValue, githubExists := claims["github_id"] + if !githubExists { + c.AbortWithStatusJSON(401, gin.H{"error": "missing github_id in token"}) + return + } + + githubIDFloat, ok := githubIDValue.(float64) + if !ok { + c.AbortWithStatusJSON(401, gin.H{"error": "invalid github_id type in token"}) + return + } + + loginValue, loginExists := claims["login"] + if !loginExists { + c.AbortWithStatusJSON(401, gin.H{"error": "missing login in token"}) + return + } + + login, ok := loginValue.(string) + if !ok { + c.AbortWithStatusJSON(401, gin.H{"error": "invalid login type in token"}) + return + } + + c.Set("user_id", int(idFloat)) + c.Set("github_id", int(githubIDFloat)) + c.Set("login", login) c.Next() } } + +func RequireAdmin() gin.HandlerFunc { + return func(c *gin.Context) { + githubID, exists := c.Get("github_id") + if !exists { + c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"}) + return + } + + id := githubID.(int) + if id != 173489813 { + c.AbortWithStatusJSON(403, gin.H{"error": "access denied"}) + return + } + + c.Next() + } +} + +func ValidateJWT(tokenString string) (*storage.User, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return jwtSecret, nil + }) + + if err != nil || !token.Valid { + return nil, err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid claims") + } + + if exp, ok := claims["exp"].(float64); ok { + if time.Now().Unix() > int64(exp) { + return nil, fmt.Errorf("token expired") + } + } + idFloat, ok := claims["id"].(float64) + if !ok { + return nil, fmt.Errorf("invalid id in token") + } + + githubIDFloat, ok := claims["github_id"].(float64) + if !ok { + return nil, fmt.Errorf("invalid github_id in token") + } + + login, ok := claims["login"].(string) + if !ok { + return nil, fmt.Errorf("invalid login in token") + } + + email, ok := claims["email"].(string) + if !ok { + return nil, fmt.Errorf("invalid email in token") + } + + avatarURL, _ := claims["avatar_url"].(string) + + user := &storage.User{ + ID: int(idFloat), + GithubID: int(githubIDFloat), + GithubLogin: login, + Email: email, + AvatarURL: avatarURL, + } + + return user, nil +} diff --git a/backend/internal/handlers/auth_handlers.go b/backend/internal/handlers/auth_handlers.go index c404a39..4ad0082 100644 --- a/backend/internal/handlers/auth_handlers.go +++ b/backend/internal/handlers/auth_handlers.go @@ -4,6 +4,8 @@ import ( "encoding/json" "os" + "strings" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/auth" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" @@ -49,7 +51,7 @@ func NewAuthHandlers(repo repositories.AuthRepository) *AuthHandlers { // @Description Redirects to GitHub authorization // @Tags auth // @Success 302 -// @Router /api/v1/auth/github [get] +// @Router /auth/github [get] func (h *AuthHandlers) LoginGithub(c *gin.Context) { url := h.config.AuthCodeURL("state", oauth2.AccessTypeOnline) h.logger.Info("Redirect to GitHub: " + url) @@ -73,7 +75,7 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) { code := c.Query("code") if code == "" { h.logger.Error("missing code") - c.JSON(400, gin.H{"error": "missing code"}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=missing_code") return } @@ -82,7 +84,7 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) { token, err := h.config.Exchange(c.Request.Context(), code) if err != nil { h.logger.Error("Exchange failed: " + err.Error()) - c.JSON(500, gin.H{"error": "exchange failed", "details": err.Error()}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=auth_failed") return } @@ -90,7 +92,7 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) { resp, err := client.Get("https://api.github.com/user") if err != nil { h.logger.Error("Get failed: " + err.Error()) - c.JSON(500, gin.H{"error": "get request failed to github", "details": err.Error()}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=github_api_failed") return } @@ -98,14 +100,14 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) { err = json.NewDecoder(resp.Body).Decode(&ghUser) if err != nil { h.logger.Error("Decode failed: " + err.Error()) - c.JSON(500, gin.H{"error": "decode failed", "details": err.Error()}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=decode_failed") return } isreg, err := h.repo.IsRegistered(c.Request.Context(), ghUser.GithubID) if err != nil { h.logger.Error("Database check failed: " + err.Error()) - c.JSON(500, gin.H{"error": "database error", "details": err.Error()}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=database_error") return } @@ -114,10 +116,23 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) { id, err = h.repo.Register(c.Request.Context(), ghUser) if err != nil { h.logger.Error("Registration failed: " + err.Error()) - c.JSON(500, gin.H{"error": "registration failed", "details": err.Error()}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=registration_failed") return } + } else { + h.logger.Info("Existing user, fetching data: " + ghUser.GithubLogin) + user, err := h.repo.GetUserByGithubID(c.Request.Context(), ghUser.GithubID) + if err != nil { + h.logger.Error("Failed to fetch user: " + err.Error()) + c.Redirect(302, "https://d3m0k1d.ru/login?error=user_fetch_failed") + return + } + id = user.ID + ghUser.GithubLogin = user.GithubLogin + ghUser.Email = user.Email + ghUser.AvatarURL = user.AvatarURL } + user := storage.User{ ID: id, GithubID: ghUser.GithubID, @@ -125,17 +140,51 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) { Email: ghUser.Email, AvatarURL: ghUser.AvatarURL, } + jwtToken, err := auth.GenerateJWT(user) if err != nil { h.logger.Error("JWT generation failed: " + err.Error()) - c.JSON(500, gin.H{"error": "token generation failed", "details": err.Error()}) + c.Redirect(302, "https://d3m0k1d.ru/login?error=token_failed") return } h.logger.Info("Authentication successful for user: " + ghUser.GithubLogin) + c.Redirect(302, "https://d3m0k1d.ru/auth/callback#token="+jwtToken) +} + +// GetSession godoc +// @Summary Get user session +// @Description Returns user session data +// @Tags auth +// @Produce json +// @Success 200 {object} map[string]interface{} "Session data" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /session [get] +func (h *AuthHandlers) GetSession(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(401, gin.H{"error": "unauthorized"}) + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == authHeader { + c.JSON(401, gin.H{"error": "invalid authorization header"}) + return + } + + user, err := auth.ValidateJWT(tokenString) + if err != nil { + c.JSON(401, gin.H{"error": "invalid token"}) + return + } + c.JSON(200, gin.H{ - "token": jwtToken, - "user": ghUser, + "user": gin.H{ + "name": user.GithubLogin, + "email": user.Email, + "avatar": user.AvatarURL, + }, }) } diff --git a/backend/internal/handlers/post_handlers.go b/backend/internal/handlers/post_handlers.go index 0e074d4..475d524 100644 --- a/backend/internal/handlers/post_handlers.go +++ b/backend/internal/handlers/post_handlers.go @@ -4,6 +4,7 @@ import ( "strconv" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" "github.com/gin-gonic/gin" @@ -24,17 +25,25 @@ func NewPostHandlers(repo repositories.PostRepository) *PostHandlers { // @Tags posts // @Accept json // @Produce json -// @Success 200 {object} []storage.PostReq +// @Success 200 {object} models.SuccessResponse{data=[]storage.PostReq} +// @Failure 404 {object} models.ErrorResponse "No Post found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Router /posts [get] func (h *PostHandlers) GetPosts(c *gin.Context) { var result []storage.PostReq result, err := h.repo.GetAll(c.Request.Context()) if err != nil { h.logger.Error("error request: " + err.Error()) - c.Status(500) + models.Error(c, 500, "Internal server error", err.Error()) + return + } + if result == nil { + models.Error(c, 404, "No Post found", "") + return } h.logger.Info("200 OK GET /posts") - c.JSON(200, result) + models.Success(c, result) } // GetPost godoc @@ -44,26 +53,39 @@ func (h *PostHandlers) GetPosts(c *gin.Context) { // @Accept json // @Param id path int true "Post ID" // @Produce json -// @Success 200 {object} storage.PostReq -// @Failure 400 {object} map[string]string "Invalid ID format" -// @Failure 500 {object} map[string]string "Internal server error" +// @Success 200 {object} models.SuccessResponse{data=storage.PostReq} +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Post not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /posts/{id} [get] func (h *PostHandlers) GetPost(c *gin.Context) { var result storage.PostReq + last_id, err := h.repo.GetLastID(c.Request.Context()) + if err != nil { + h.logger.Error("error request: " + err.Error()) + models.Error(c, 500, "Internal server error", err.Error()) + return + } id_p := c.Param("id") id, err := strconv.Atoi(id_p) if err != nil { h.logger.Error("error request: " + err.Error()) - c.Status(500) + models.Error(c, 400, "Invalid ID format", err.Error()) + return + } + if id > last_id { + models.Error(c, 404, "Post not found", "") + return } result, err = h.repo.GetByID(c.Request.Context(), id) if err != nil { h.logger.Error("error request: " + err.Error()) - c.Status(500) + models.Error(c, 500, "Internal server error", err.Error()) + return } h.logger.Info("200 OK GET /posts/" + id_p) - c.JSON(200, result) - // TODO: added validaton for 400 response + models.Success(c, result) + } // CreatePost godoc @@ -73,14 +95,15 @@ func (h *PostHandlers) GetPost(c *gin.Context) { // @Accept json // @Produce json // @Param post body storage.PostCreate true "Post data" -// @Success 200 {object} storage.PostReq -// @Failure 400 {object} gin.H +// @Security Bearer +// @Success 200 {object} models.SuccessResponse{data=storage.Post} +// @Failure 400 {object} models.ErrorResponse "Invalid request" // @Router /posts [post] func (h *PostHandlers) CreatePost(c *gin.Context) { var req storage.PostCreate if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, gin.H{"error": err.Error()}) + models.Error(c, 400, "Invalid request", err.Error()) return } @@ -90,11 +113,11 @@ func (h *PostHandlers) CreatePost(c *gin.Context) { } if err := h.repo.Create(c.Request.Context(), post); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + models.Error(c, 500, "Internal server error", err.Error()) return } - c.JSON(200, post) + models.Success(c, post) } // UpdatePost godoc @@ -105,27 +128,32 @@ func (h *PostHandlers) CreatePost(c *gin.Context) { // @Param id path int true "Post ID" // @Param post body storage.PostCreate true "Post data" // @Produce json -// @Success 200 {object} storage.PostCreate +// @Security Bearer +// @Success 200 {object} models.SuccessResponse{data=storage.PostCreate} +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// Failure 404 {object} models.ErrorResponse "Post not found" // @Router /posts/{id} [put] func (h *PostHandlers) UpdatePost(c *gin.Context) { id_p := c.Param("id") id, err := strconv.Atoi(id_p) if err != nil { h.logger.Error("error request: " + err.Error()) - c.Status(500) + models.Error(c, 500, "Internal server error", err.Error()) + return } var req storage.Post if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, gin.H{"error": err.Error()}) + models.Error(c, 400, "Invalid request", err.Error()) return } err = h.repo.Update(c.Request.Context(), id, req) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + models.Error(c, 500, "Internal server error", err.Error()) return } - c.JSON(200, req) + models.Success(c, req) h.logger.Info("200 OK PUT /posts/" + id_p) } @@ -133,9 +161,34 @@ func (h *PostHandlers) UpdatePost(c *gin.Context) { // @Summary Delete post // @Description Delete post // @Tags posts +// @Param id path int true "Post ID" // @Accept json // @Produce json -// @Success 200 {object} storage.Post +// @Security Bearer +// @Failure 404 {object} models.ErrorResponse "Post not found" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Success 200 {object} models.SuccessResponse{data=storage.Post} // @Router /posts/{id} [delete] -func DeletePost(c *gin.Context) { +func (h *PostHandlers) DeletePost(c *gin.Context) { + id_p := c.Param("id") + id, err := strconv.Atoi(id_p) + if err != nil { + h.logger.Error("error request: " + err.Error()) + models.Error(c, 400, "Invalid ID format", err.Error()) + return + } + exsist := h.repo.IsExist(c.Request.Context(), id) + if !exsist { + models.Error(c, 404, "Post not found", "") + return + } + err = h.repo.Delete(c.Request.Context(), id) + if err != nil { + models.Error(c, 500, "Internal server error", err.Error()) + return + } + h.logger.Info("200 OK DELETE /posts/" + id_p) + models.Success(c, "Post deleted") + } diff --git a/backend/internal/handlers/posts_test.go b/backend/internal/handlers/posts_test.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/backend/internal/handlers/posts_test.go @@ -0,0 +1 @@ +package handlers diff --git a/backend/internal/handlers/registry_handlers.go b/backend/internal/handlers/registry_handlers.go index 2109afb..c4a916a 100644 --- a/backend/internal/handlers/registry_handlers.go +++ b/backend/internal/handlers/registry_handlers.go @@ -3,6 +3,7 @@ package handlers import ( "database/sql" + "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/auth" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" "github.com/gin-gonic/gin" ) @@ -14,12 +15,14 @@ func Register(router *gin.Engine, db *sql.DB) { v1 := router.Group("api/v1") v1.GET("/callback/github", handler_auth.CallbackGithub) v1.GET("/auth/github", handler_auth.LoginGithub) + v1.GET("/session", auth.JWTMiddleware(), handler_auth.GetSession) posts := v1.Group("posts") { + posts.GET("/", handler_posts.GetPosts) posts.GET("/:id", handler_posts.GetPost) - posts.POST("/", handler_posts.CreatePost) - posts.PUT("/:id", handler_posts.UpdatePost) - posts.DELETE("/:id", DeletePost) + posts.POST("/", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.CreatePost) + posts.PUT("/:id", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.UpdatePost) + posts.DELETE("/:id", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.DeletePost) } } diff --git a/backend/internal/models/responses.go b/backend/internal/models/responses.go new file mode 100644 index 0000000..121a88d --- /dev/null +++ b/backend/internal/models/responses.go @@ -0,0 +1,27 @@ +package models + +import ( + "github.com/gin-gonic/gin" +) + +type SuccessResponse struct { + Data interface{} `json:"data"` +} + +type ErrorResponse struct { + Error ErrorDetail `json:"error"` +} + +type ErrorDetail struct { + Code int `json:"code"` + Message string `json:"message"` + Detail string `json:"detail"` +} + +func Success(c *gin.Context, data interface{}) { + c.JSON(200, SuccessResponse{Data: data}) +} + +func Error(c *gin.Context, code int, message string, detail string) { + c.JSON(code, ErrorResponse{Error: ErrorDetail{Code: code, Message: message, Detail: detail}}) +} diff --git a/backend/internal/repositories/auth_repository.go b/backend/internal/repositories/auth_repository.go index 5361516..15d163c 100644 --- a/backend/internal/repositories/auth_repository.go +++ b/backend/internal/repositories/auth_repository.go @@ -22,28 +22,56 @@ func NewAuthRepository(db *sql.DB) AuthRepository { func (a *authRepository) Register(ctx context.Context, user storage.UserReg) (int, error) { var id int - _, err := a.db.Exec( + _, err := a.db.ExecContext(ctx, "INSERT INTO users(email, github_id, github_login, avatar_url) VALUES(?, ?, ?, ?)", + user.Email, user.GithubID, user.GithubLogin, user.AvatarURL, ) if err != nil { - a.logger.Error("error request: " + err.Error()) + a.logger.Error("error insert: " + err.Error()) return 0, err } - row := a.db.QueryRow("SELECT id FROM users WHERE github_id = ?", user.GithubID) + + row := a.db.QueryRowContext(ctx, "SELECT id FROM users WHERE github_id = ?", user.GithubID) err = row.Scan(&id) if err != nil { a.logger.Error("error scan: " + err.Error()) return 0, err } - a.logger.Info("User registered:", "email", user.Email) + a.logger.Info("User registered: " + user.Email) return id, nil } func (a *authRepository) IsRegistered(ctx context.Context, github_id int) (bool, error) { - row := a.db.QueryRow("SELECT id FROM users WHERE github_id = ?", github_id) - if row != nil { - return true, nil + var id int + err := a.db.QueryRowContext(ctx, "SELECT id FROM users WHERE github_id = ?", github_id). + Scan(&id) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, err } - return false, nil + return true, nil +} + +func (r *authRepository) GetUserByGithubID( + ctx context.Context, + githubID int, +) (*storage.User, error) { + var user storage.User + query := `SELECT id, github_id, github_login, email, avatar_url FROM users WHERE github_id = ?` + + err := r.db.QueryRowContext(ctx, query, githubID).Scan( + &user.ID, + &user.GithubID, + &user.GithubLogin, + &user.Email, + &user.AvatarURL, + ) + if err != nil { + return nil, err + } + + return &user, nil } diff --git a/backend/internal/repositories/interface.go b/backend/internal/repositories/interface.go index b49bdcf..d81e6ef 100644 --- a/backend/internal/repositories/interface.go +++ b/backend/internal/repositories/interface.go @@ -9,6 +9,8 @@ import ( type PostRepository interface { GetAll(ctx context.Context) ([]storage.PostReq, error) GetByID(ctx context.Context, id int) (storage.PostReq, error) + GetLastID(ctx context.Context) (int, error) + IsExist(ctx context.Context, id int) bool Create(ctx context.Context, post storage.Post) error Update(ctx context.Context, id int, post storage.Post) error Delete(ctx context.Context, id int) error @@ -17,4 +19,5 @@ type PostRepository interface { type AuthRepository interface { Register(ctx context.Context, user storage.UserReg) (int, error) IsRegistered(ctx context.Context, github_id int) (bool, error) + GetUserByGithubID(ctx context.Context, githubID int) (*storage.User, error) } diff --git a/backend/internal/repositories/post_repository.go b/backend/internal/repositories/post_repository.go index 7917b3f..d248b14 100644 --- a/backend/internal/repositories/post_repository.go +++ b/backend/internal/repositories/post_repository.go @@ -92,6 +92,33 @@ func (p *postRepository) Update(ctx context.Context, id int, post storage.Post) } func (p *postRepository) Delete(ctx context.Context, id int) error { - + _, err := p.db.Exec("DELETE FROM posts WHERE id = ?", id) + if err != nil { + return err + } + p.logger.Info("Post deleted:", "id", id) return nil } + +func (p *postRepository) GetLastID(ctx context.Context) (int, error) { + var id int + row := p.db.QueryRow("SELECT id FROM posts ORDER BY id DESC LIMIT 1") + err := row.Scan(&id) + if err != nil { + p.logger.Error("error scan: " + err.Error()) + } + return id, nil +} + +func (p *postRepository) IsExist(ctx context.Context, id int) bool { + var exists int + err := p.db.QueryRowContext(ctx, "SELECT 1 FROM posts WHERE id = ? LIMIT 1", id).Scan(&exists) + if err != nil { + if err == sql.ErrNoRows { + return false + } + p.logger.Error("error checking post existence: " + err.Error()) + return false + } + return true +} diff --git a/backend/internal/storage/db.go b/backend/internal/storage/db.go index 94c53af..117860a 100644 --- a/backend/internal/storage/db.go +++ b/backend/internal/storage/db.go @@ -8,9 +8,9 @@ import ( _ "modernc.org/sqlite" ) -var db_path = os.Getenv( - "DB_PATH", -) + "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON" +var path = os.Getenv("DB_PATH") + +var params = "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON" func CreateTables(db *sql.DB) error { logger := logger.New(false) @@ -24,7 +24,7 @@ func CreateTables(db *sql.DB) error { } func OpenSession() (*sql.DB, error) { - db, err := sql.Open("sqlite", db_path) + db, err := sql.Open("sqlite", path+params) if err != nil { return nil, err } diff --git a/backend/internal/storage/models.go b/backend/internal/storage/models.go index c9503b4..ea70986 100644 --- a/backend/internal/storage/models.go +++ b/backend/internal/storage/models.go @@ -27,8 +27,8 @@ type User struct { } type UserReg struct { - Email string `db:"email" json:"email"` - GithubID int `db:"github_id" json:"github_id"` - GithubLogin string `db:"github_login" json:"github_login"` - AvatarURL string `db:"avatar_url" json:"avatar_url"` + Email string `json:"email"` + GithubID int `json:"id"` + GithubLogin string `json:"login"` + AvatarURL string `json:"avatar_url"` } diff --git a/frontend/index.html b/frontend/index.html index b8aa7e8..50878e7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,42 +1,117 @@ +
+ + + + + + + + + + +Completing authentication...
+
+ {`
+ ██╗ ██████╗ ██████╗ ██╗███╗ ██╗
+ ██║ ██╔═══██╗██╔════╝ ██║████╗ ██║
+ ██║ ██║ ██║██║ ███╗██║██╔██╗ ██║
+ ██║ ██║ ██║██║ ██║██║██║╚██╗██║
+ ███████╗╚██████╔╝╚██████╔╝██║██║ ╚████║
+ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝
+
+ ╔════════════════════════════════════╗
+ ║ Secure GitHub Authentication ║
+ ╚════════════════════════════════════╝
+`}
+
+
+ {/* Login Card */}
+ + Sign in to continue to your account +
+ + {/* GitHub Login Button */} + + + {/* Divider */} ++ By signing in, you agree to our{" "} + + terms of service + +
+