Merge pull request 'develop' (#9) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 5m11s
Frontend deploy / deploy-frontend (push) Successful in 1m53s

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-02-14 19:40:34 +00:00
30 changed files with 1612 additions and 254 deletions

View File

@@ -1,10 +1,6 @@
name: Backend ci name: Backend ci
on: on:
push:
branches:
- master
- develop
pull_request: pull_request:
branches: branches:
- master - master

View File

@@ -15,5 +15,6 @@ FROM alpine:3.23.0
COPY --from=builder /app/backend . COPY --from=builder /app/backend .
EXPOSE 8080 EXPOSE 8080
RUN apk add --no-cache curl
HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit 1 HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit 1
CMD ["./backend"] CMD ["./backend"]

View File

@@ -1,4 +1,4 @@
.PHONY: test build clean lint dev .PHONY: test build clean lint dev run-docker docs-upd docker
test: test:
@@ -19,3 +19,16 @@ lint:
dev: dev:
swag init -g ./cmd/main.go --parseDependency --parseInternal swag init -g ./cmd/main.go --parseDependency --parseInternal
go run ./cmd/main.go 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

View File

@@ -10,6 +10,10 @@ import (
ginSwagger "github.com/swaggo/gin-swagger" 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() { func main() {
log := logger.New(false) log := logger.New(false)

View File

@@ -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:

View File

@@ -15,22 +15,58 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/auth/github": {
"get": {
"description": "Redirects to GitHub authorization",
"tags": [
"auth"
],
"summary": "Start GitHub OAuth login",
"responses": {
"302": {
"description": "Found"
}
}
}
},
"/callback/github": { "/callback/github": {
"get": { "get": {
"description": "Callback for oauth2 providers", "description": "Exchanges authorization code for access token",
"consumes": [
"application/json"
],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Callback for oauth2 providers", "summary": "GitHub OAuth callback",
"parameters": [
{
"type": "string",
"description": "Authorization code",
"name": "code",
"in": "query",
"required": true
}
],
"responses": { "responses": {
"200": { "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": { "schema": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@@ -58,6 +94,14 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
@@ -65,8 +109,35 @@ const docTemplate = `{
} }
} }
} }
]
}
},
"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": { "post": {
"security": [
{
"Bearer": []
}
],
"description": "Create new post", "description": "Create new post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -93,13 +164,25 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "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": { "400": {
"description": "Bad Request", "description": "Invalid request",
"schema": { "schema": {
"$ref": "#/definitions/gin.H" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
} }
} }
@@ -131,30 +214,47 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"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" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
} }
}
}
]
}
}, },
"400": { "400": {
"description": "Invalid ID format", "description": "Invalid ID format",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
"additionalProperties": {
"type": "string"
} }
},
"404": {
"description": "Post not found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal server error", "description": "Internal server error",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
"additionalProperties": {
"type": "string"
}
} }
} }
} }
}, },
"put": { "put": {
"security": [
{
"Bearer": []
}
],
"description": "Update post", "description": "Update post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -188,12 +288,41 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"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" "$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": { "delete": {
"security": [
{
"Bearer": []
}
],
"description": "Delete post", "description": "Delete post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -205,21 +334,114 @@ const docTemplate = `{
"posts" "posts"
], ],
"summary": "Delete post", "summary": "Delete post",
"parameters": [
{
"type": "integer",
"description": "Post ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"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" "$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"
}
}
}
}
} }
} }
}, },
"definitions": { "definitions": {
"gin.H": { "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail": {
"type": "object", "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": { "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": {
"type": "object", "type": "object",
@@ -227,7 +449,7 @@ const docTemplate = `{
"content": { "content": {
"type": "string" "type": "string"
}, },
"createdAt": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "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"
}
} }
}` }`

View File

@@ -4,22 +4,58 @@
"contact": {} "contact": {}
}, },
"paths": { "paths": {
"/auth/github": {
"get": {
"description": "Redirects to GitHub authorization",
"tags": [
"auth"
],
"summary": "Start GitHub OAuth login",
"responses": {
"302": {
"description": "Found"
}
}
}
},
"/callback/github": { "/callback/github": {
"get": { "get": {
"description": "Callback for oauth2 providers", "description": "Exchanges authorization code for access token",
"consumes": [
"application/json"
],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Callback for oauth2 providers", "summary": "GitHub OAuth callback",
"parameters": [
{
"type": "string",
"description": "Authorization code",
"name": "code",
"in": "query",
"required": true
}
],
"responses": { "responses": {
"200": { "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": { "schema": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@@ -47,6 +83,14 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
@@ -54,8 +98,35 @@
} }
} }
} }
]
}
},
"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": { "post": {
"security": [
{
"Bearer": []
}
],
"description": "Create new post", "description": "Create new post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -82,13 +153,25 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "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": { "400": {
"description": "Bad Request", "description": "Invalid request",
"schema": { "schema": {
"$ref": "#/definitions/gin.H" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
} }
} }
@@ -120,30 +203,47 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"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" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
} }
}
}
]
}
}, },
"400": { "400": {
"description": "Invalid ID format", "description": "Invalid ID format",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
"additionalProperties": {
"type": "string"
} }
},
"404": {
"description": "Post not found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal server error", "description": "Internal server error",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
"additionalProperties": {
"type": "string"
}
} }
} }
} }
}, },
"put": { "put": {
"security": [
{
"Bearer": []
}
],
"description": "Update post", "description": "Update post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -177,12 +277,41 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"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" "$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": { "delete": {
"security": [
{
"Bearer": []
}
],
"description": "Delete post", "description": "Delete post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -194,21 +323,114 @@
"posts" "posts"
], ],
"summary": "Delete post", "summary": "Delete post",
"parameters": [
{
"type": "integer",
"description": "Post ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"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" "$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"
}
}
}
}
} }
} }
}, },
"definitions": { "definitions": {
"gin.H": { "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail": {
"type": "object", "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": { "gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": {
"type": "object", "type": "object",
@@ -216,7 +438,7 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"createdAt": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
@@ -252,5 +474,13 @@
} }
} }
} }
},
"securityDefinitions": {
"Bearer": {
"description": "Type \"Bearer\" followed by a space and the JWT token.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
} }
} }

View File

@@ -1,12 +1,27 @@
definitions: definitions:
gin.H: gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorDetail:
additionalProperties: {} 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 type: object
gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post: gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post:
properties: properties:
content: content:
type: string type: string
createdAt: created_at:
type: string type: string
id: id:
type: integer type: integer
@@ -32,21 +47,45 @@ definitions:
info: info:
contact: {} contact: {}
paths: paths:
/auth/github:
get:
description: Redirects to GitHub authorization
responses:
"302":
description: Found
summary: Start GitHub OAuth login
tags:
- auth
/callback/github: /callback/github:
get: get:
consumes: description: Exchanges authorization code for access token
- application/json parameters:
description: Callback for oauth2 providers - description: Authorization code
in: query
name: code
required: true
type: string
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: Access token
schema:
additionalProperties: true
type: object
"400":
description: Missing code
schema: schema:
additionalProperties: additionalProperties:
type: string type: string
type: object type: object
summary: Callback for oauth2 providers "500":
description: Exchange failed
schema:
additionalProperties:
type: string
type: object
summary: GitHub OAuth callback
tags: tags:
- auth - auth
/posts: /posts:
@@ -60,9 +99,26 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
allOf:
- $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
- properties:
data:
items: items:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
type: array 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 summary: Get all posts
tags: tags:
- posts - posts
@@ -83,11 +139,18 @@ paths:
"200": "200":
description: OK description: OK
schema: 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": "400":
description: Bad Request description: Invalid request
schema: schema:
$ref: '#/definitions/gin.H' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
security:
- Bearer: []
summary: Create post summary: Create post
tags: tags:
- posts - posts
@@ -96,13 +159,38 @@ paths:
consumes: consumes:
- application/json - application/json
description: Delete post description: Delete post
parameters:
- description: Post ID
in: path
name: id
required: true
type: integer
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
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' $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 summary: Delete post
tags: tags:
- posts - posts
@@ -122,19 +210,24 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
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' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
type: object
"400": "400":
description: Invalid ID format description: Invalid ID format
schema: schema:
additionalProperties: $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
type: string "404":
type: object description: Post not found
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"500": "500":
description: Internal server error description: Internal server error
schema: schema:
additionalProperties: $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
type: string
type: object
summary: Get post by id summary: Get post by id
tags: tags:
- posts - posts
@@ -160,8 +253,49 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
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' $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 summary: Update post
tags: tags:
- posts - 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" swagger: "2.0"

View File

@@ -2,23 +2,29 @@ module gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend
go 1.25.6 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 ( require (
github.com/KyleBanks/depth v1.2.1 // indirect 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/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.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/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // 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/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // 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/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils 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/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // 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/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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // 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/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/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // 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/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.32.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.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/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // 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/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.44.3 // indirect
) )

View File

@@ -1,30 +1,23 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 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 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= 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 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= 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 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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.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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 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/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 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= 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.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= 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/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 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= 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 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= 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 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= 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= 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/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 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= 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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 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= 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/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 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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= 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/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 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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/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 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 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/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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.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.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.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.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.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 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= 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.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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 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= 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 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 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 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 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 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 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=

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -16,7 +17,11 @@ func GenerateJWT(user storage.User) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"id": user.ID, "id": user.ID,
"email": user.Email, "email": user.Email,
"login": user.GithubLogin,
"github_id": user.GithubID, "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) tokenString, err := token.SignedString(jwtSecret)
if err != nil { if err != nil {
@@ -35,7 +40,6 @@ func JWTMiddleware() gin.HandlerFunc {
tokenString := strings.TrimPrefix(auth, "Bearer ") tokenString := strings.TrimPrefix(auth, "Bearer ")
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
@@ -53,8 +57,118 @@ func JWTMiddleware() gin.HandlerFunc {
return return
} }
c.Set("user_id", int(claims["id"].(float64))) idValue, idExists := claims["id"]
c.Set("login", claims["login"].(string)) 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() 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
}

View File

@@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/auth" "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/logger"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories"
@@ -49,7 +51,7 @@ func NewAuthHandlers(repo repositories.AuthRepository) *AuthHandlers {
// @Description Redirects to GitHub authorization // @Description Redirects to GitHub authorization
// @Tags auth // @Tags auth
// @Success 302 // @Success 302
// @Router /api/v1/auth/github [get] // @Router /auth/github [get]
func (h *AuthHandlers) LoginGithub(c *gin.Context) { func (h *AuthHandlers) LoginGithub(c *gin.Context) {
url := h.config.AuthCodeURL("state", oauth2.AccessTypeOnline) url := h.config.AuthCodeURL("state", oauth2.AccessTypeOnline)
h.logger.Info("Redirect to GitHub: " + url) h.logger.Info("Redirect to GitHub: " + url)
@@ -73,7 +75,7 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) {
code := c.Query("code") code := c.Query("code")
if code == "" { if code == "" {
h.logger.Error("missing 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 return
} }
@@ -82,7 +84,7 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) {
token, err := h.config.Exchange(c.Request.Context(), code) token, err := h.config.Exchange(c.Request.Context(), code)
if err != nil { if err != nil {
h.logger.Error("Exchange failed: " + err.Error()) 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 return
} }
@@ -90,7 +92,7 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) {
resp, err := client.Get("https://api.github.com/user") resp, err := client.Get("https://api.github.com/user")
if err != nil { if err != nil {
h.logger.Error("Get failed: " + err.Error()) 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 return
} }
@@ -98,14 +100,14 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) {
err = json.NewDecoder(resp.Body).Decode(&ghUser) err = json.NewDecoder(resp.Body).Decode(&ghUser)
if err != nil { if err != nil {
h.logger.Error("Decode failed: " + err.Error()) 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 return
} }
isreg, err := h.repo.IsRegistered(c.Request.Context(), ghUser.GithubID) isreg, err := h.repo.IsRegistered(c.Request.Context(), ghUser.GithubID)
if err != nil { if err != nil {
h.logger.Error("Database check failed: " + err.Error()) 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 return
} }
@@ -114,10 +116,23 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) {
id, err = h.repo.Register(c.Request.Context(), ghUser) id, err = h.repo.Register(c.Request.Context(), ghUser)
if err != nil { if err != nil {
h.logger.Error("Registration failed: " + err.Error()) 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 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{ user := storage.User{
ID: id, ID: id,
GithubID: ghUser.GithubID, GithubID: ghUser.GithubID,
@@ -125,17 +140,51 @@ func (h *AuthHandlers) CallbackGithub(c *gin.Context) {
Email: ghUser.Email, Email: ghUser.Email,
AvatarURL: ghUser.AvatarURL, AvatarURL: ghUser.AvatarURL,
} }
jwtToken, err := auth.GenerateJWT(user) jwtToken, err := auth.GenerateJWT(user)
if err != nil { if err != nil {
h.logger.Error("JWT generation failed: " + err.Error()) 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 return
} }
h.logger.Info("Authentication successful for user: " + ghUser.GithubLogin) 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{ c.JSON(200, gin.H{
"token": jwtToken, "user": gin.H{
"user": ghUser, "name": user.GithubLogin,
"email": user.Email,
"avatar": user.AvatarURL,
},
}) })
} }

View File

@@ -4,6 +4,7 @@ import (
"strconv" "strconv"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "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/repositories"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -24,17 +25,25 @@ func NewPostHandlers(repo repositories.PostRepository) *PostHandlers {
// @Tags posts // @Tags posts
// @Accept json // @Accept json
// @Produce 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] // @Router /posts [get]
func (h *PostHandlers) GetPosts(c *gin.Context) { func (h *PostHandlers) GetPosts(c *gin.Context) {
var result []storage.PostReq var result []storage.PostReq
result, err := h.repo.GetAll(c.Request.Context()) result, err := h.repo.GetAll(c.Request.Context())
if err != nil { if err != nil {
h.logger.Error("error request: " + err.Error()) 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") h.logger.Info("200 OK GET /posts")
c.JSON(200, result) models.Success(c, result)
} }
// GetPost godoc // GetPost godoc
@@ -44,26 +53,39 @@ func (h *PostHandlers) GetPosts(c *gin.Context) {
// @Accept json // @Accept json
// @Param id path int true "Post ID" // @Param id path int true "Post ID"
// @Produce json // @Produce json
// @Success 200 {object} storage.PostReq // @Success 200 {object} models.SuccessResponse{data=storage.PostReq}
// @Failure 400 {object} map[string]string "Invalid ID format" // @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 500 {object} map[string]string "Internal server error" // @Failure 404 {object} models.ErrorResponse "Post not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /posts/{id} [get] // @Router /posts/{id} [get]
func (h *PostHandlers) GetPost(c *gin.Context) { func (h *PostHandlers) GetPost(c *gin.Context) {
var result storage.PostReq 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_p := c.Param("id")
id, err := strconv.Atoi(id_p) id, err := strconv.Atoi(id_p)
if err != nil { if err != nil {
h.logger.Error("error request: " + err.Error()) 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) result, err = h.repo.GetByID(c.Request.Context(), id)
if err != nil { if err != nil {
h.logger.Error("error request: " + err.Error()) 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) h.logger.Info("200 OK GET /posts/" + id_p)
c.JSON(200, result) models.Success(c, result)
// TODO: added validaton for 400 response
} }
// CreatePost godoc // CreatePost godoc
@@ -73,14 +95,15 @@ func (h *PostHandlers) GetPost(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param post body storage.PostCreate true "Post data" // @Param post body storage.PostCreate true "Post data"
// @Success 200 {object} storage.PostReq // @Security Bearer
// @Failure 400 {object} gin.H // @Success 200 {object} models.SuccessResponse{data=storage.Post}
// @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Router /posts [post] // @Router /posts [post]
func (h *PostHandlers) CreatePost(c *gin.Context) { func (h *PostHandlers) CreatePost(c *gin.Context) {
var req storage.PostCreate var req storage.PostCreate
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()}) models.Error(c, 400, "Invalid request", err.Error())
return return
} }
@@ -90,11 +113,11 @@ func (h *PostHandlers) CreatePost(c *gin.Context) {
} }
if err := h.repo.Create(c.Request.Context(), post); err != nil { 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 return
} }
c.JSON(200, post) models.Success(c, post)
} }
// UpdatePost godoc // UpdatePost godoc
@@ -105,27 +128,32 @@ func (h *PostHandlers) CreatePost(c *gin.Context) {
// @Param id path int true "Post ID" // @Param id path int true "Post ID"
// @Param post body storage.PostCreate true "Post data" // @Param post body storage.PostCreate true "Post data"
// @Produce json // @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] // @Router /posts/{id} [put]
func (h *PostHandlers) UpdatePost(c *gin.Context) { func (h *PostHandlers) UpdatePost(c *gin.Context) {
id_p := c.Param("id") id_p := c.Param("id")
id, err := strconv.Atoi(id_p) id, err := strconv.Atoi(id_p)
if err != nil { if err != nil {
h.logger.Error("error request: " + err.Error()) h.logger.Error("error request: " + err.Error())
c.Status(500) models.Error(c, 500, "Internal server error", err.Error())
return
} }
var req storage.Post var req storage.Post
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()}) models.Error(c, 400, "Invalid request", err.Error())
return return
} }
err = h.repo.Update(c.Request.Context(), id, req) err = h.repo.Update(c.Request.Context(), id, req)
if err != nil { if err != nil {
c.JSON(500, gin.H{"error": err.Error()}) models.Error(c, 500, "Internal server error", err.Error())
return return
} }
c.JSON(200, req) models.Success(c, req)
h.logger.Info("200 OK PUT /posts/" + id_p) h.logger.Info("200 OK PUT /posts/" + id_p)
} }
@@ -133,9 +161,34 @@ func (h *PostHandlers) UpdatePost(c *gin.Context) {
// @Summary Delete post // @Summary Delete post
// @Description Delete post // @Description Delete post
// @Tags posts // @Tags posts
// @Param id path int true "Post ID"
// @Accept json // @Accept json
// @Produce 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] // @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")
} }

View File

@@ -0,0 +1 @@
package handlers

View File

@@ -3,6 +3,7 @@ package handlers
import ( import (
"database/sql" "database/sql"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/auth"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -14,12 +15,14 @@ func Register(router *gin.Engine, db *sql.DB) {
v1 := router.Group("api/v1") v1 := router.Group("api/v1")
v1.GET("/callback/github", handler_auth.CallbackGithub) v1.GET("/callback/github", handler_auth.CallbackGithub)
v1.GET("/auth/github", handler_auth.LoginGithub) v1.GET("/auth/github", handler_auth.LoginGithub)
v1.GET("/session", auth.JWTMiddleware(), handler_auth.GetSession)
posts := v1.Group("posts") posts := v1.Group("posts")
{ {
posts.GET("/", handler_posts.GetPosts) posts.GET("/", handler_posts.GetPosts)
posts.GET("/:id", handler_posts.GetPost) posts.GET("/:id", handler_posts.GetPost)
posts.POST("/", handler_posts.CreatePost) posts.POST("/", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.CreatePost)
posts.PUT("/:id", handler_posts.UpdatePost) posts.PUT("/:id", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.UpdatePost)
posts.DELETE("/:id", DeletePost) posts.DELETE("/:id", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.DeletePost)
} }
} }

View File

@@ -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}})
}

View File

@@ -22,28 +22,56 @@ func NewAuthRepository(db *sql.DB) AuthRepository {
func (a *authRepository) Register(ctx context.Context, user storage.UserReg) (int, error) { func (a *authRepository) Register(ctx context.Context, user storage.UserReg) (int, error) {
var id int var id int
_, err := a.db.Exec( _, err := a.db.ExecContext(ctx,
"INSERT INTO users(email, github_id, github_login, avatar_url) VALUES(?, ?, ?, ?)", "INSERT INTO users(email, github_id, github_login, avatar_url) VALUES(?, ?, ?, ?)",
user.Email, user.GithubID, user.GithubLogin, user.AvatarURL,
) )
if err != nil { if err != nil {
a.logger.Error("error request: " + err.Error()) a.logger.Error("error insert: " + err.Error())
return 0, err 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) err = row.Scan(&id)
if err != nil { if err != nil {
a.logger.Error("error scan: " + err.Error()) a.logger.Error("error scan: " + err.Error())
return 0, err return 0, err
} }
a.logger.Info("User registered:", "email", user.Email) a.logger.Info("User registered: " + user.Email)
return id, nil return id, nil
} }
func (a *authRepository) IsRegistered(ctx context.Context, github_id int) (bool, error) { 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) var id int
if row != nil { err := a.db.QueryRowContext(ctx, "SELECT id FROM users WHERE github_id = ?", github_id).
return true, nil Scan(&id)
} if err != nil {
if err == sql.ErrNoRows {
return false, nil return false, nil
} }
return false, err
}
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
}

View File

@@ -9,6 +9,8 @@ import (
type PostRepository interface { type PostRepository interface {
GetAll(ctx context.Context) ([]storage.PostReq, error) GetAll(ctx context.Context) ([]storage.PostReq, error)
GetByID(ctx context.Context, id int) (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 Create(ctx context.Context, post storage.Post) error
Update(ctx context.Context, id int, post storage.Post) error Update(ctx context.Context, id int, post storage.Post) error
Delete(ctx context.Context, id int) error Delete(ctx context.Context, id int) error
@@ -17,4 +19,5 @@ type PostRepository interface {
type AuthRepository interface { type AuthRepository interface {
Register(ctx context.Context, user storage.UserReg) (int, error) Register(ctx context.Context, user storage.UserReg) (int, error)
IsRegistered(ctx context.Context, github_id int) (bool, error) IsRegistered(ctx context.Context, github_id int) (bool, error)
GetUserByGithubID(ctx context.Context, githubID int) (*storage.User, error)
} }

View File

@@ -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 { 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 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
}

View File

@@ -8,9 +8,9 @@ import (
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
var db_path = os.Getenv( var path = os.Getenv("DB_PATH")
"DB_PATH",
) + "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON" var params = "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON"
func CreateTables(db *sql.DB) error { func CreateTables(db *sql.DB) error {
logger := logger.New(false) logger := logger.New(false)
@@ -24,7 +24,7 @@ func CreateTables(db *sql.DB) error {
} }
func OpenSession() (*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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -27,8 +27,8 @@ type User struct {
} }
type UserReg struct { type UserReg struct {
Email string `db:"email" json:"email"` Email string `json:"email"`
GithubID int `db:"github_id" json:"github_id"` GithubID int `json:"id"`
GithubLogin string `db:"github_login" json:"github_login"` GithubLogin string `json:"login"`
AvatarURL string `db:"avatar_url" json:"avatar_url"` AvatarURL string `json:"avatar_url"`
} }

View File

@@ -1,13 +1,17 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="title" content="d3m0k1d - DevOps Engineer & InfoSec Student | Go Backend Developer" /> <meta
<meta name="keywords" name="title"
content="DevOps, InfoSec, Backend Developer, Go, Linux, Security, Portfolio, Programming, Personal Website, Personal blog, DSTU, Don State Technical Unversity, Unix" /> content="d3m0k1d - DevOps Engineer & InfoSec Student | Go Backend Developer"
/>
<meta
name="keywords"
content="DevOps, InfoSec, Backend Developer, Go, Linux, Security, Portfolio, Programming, Personal Website, Personal blog, DSTU, Don State Technical Unversity, Unix"
/>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -20,11 +24,15 @@
"@type": "EducationalOrganization", "@type": "EducationalOrganization",
"name": "Don State Technical University" "name": "Don State Technical University"
}, },
"knowsAbout": ["DevOps", "Information Security", "Backend Development", "Go", "Linux", "Infrastructure Automation"], "knowsAbout": [
"sameAs": [ "DevOps",
"https://github.com/d3m0k1d", "Information Security",
"Backend Development",
] "Go",
"Linux",
"Infrastructure Automation"
],
"sameAs": ["https://github.com/d3m0k1d"]
} }
</script> </script>
<link rel="canonical" href="https://d3m0k1d.ru" /> <link rel="canonical" href="https://d3m0k1d.ru" />
@@ -32,11 +40,78 @@
<meta name="author" content="d3m0k1d" /> <meta name="author" content="d3m0k1d" />
<meta name="robots" content="index, follow" /> <meta name="robots" content="index, follow" />
<title>d3m0k1d - DevOps Engineer & InfoSec Student</title> <title>d3m0k1d - DevOps Engineer & InfoSec Student</title>
<style>
#initial-loader {
position: fixed;
inset: 0;
background: #000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
font-family: monospace;
}
#initial-loader .spinner {
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: hsl(270, 73%, 63%);
border-radius: 50%;
animation: spin 1s linear infinite;
}
#initial-loader .text {
margin-top: 16px;
color: #666;
font-size: 14px;
}
#initial-loader .cursor {
color: hsl(270, 73%, 63%);
animation: blink 1s infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
/* Скрываем loader когда React готов */
body.loaded #initial-loader {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease-out;
}
</style>
</head> </head>
<body> <body>
<!-- Initial loader -->
<div id="initial-loader">
<div class="spinner"></div>
<div class="text">
<span class="cursor">$</span> loading<span class="cursor"
>_</span
>
</div>
</div>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -11,6 +11,7 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
@@ -2155,6 +2156,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3351,6 +3365,44 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3421,6 +3473,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -13,6 +13,7 @@
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,18 +1,40 @@
import "./App.css"; import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useEffect } from "react";
import Navigation from "./components/Navigation.tsx"; import Navigation from "./components/Navigation.tsx";
import Footer from "./components/Footer.tsx"; import Footer from "./components/Footer.tsx";
import AuthCallback from "./components/AuthCallback.tsx";
import Home from "./pages/Home.tsx"; import Home from "./pages/Home.tsx";
import About from "./components/Skills.tsx"; import About from "./components/Skills.tsx";
import Login from "./pages/Login.tsx";
function App() { function App() {
useEffect(() => {
document.body.classList.add("loaded");
}, []);
return ( return (
<BrowserRouter>
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<Navigation /> <Navigation />
<main className="flex-grow"> <main className="flex-grow">
<Routes>
<Route
path="/"
element={
<>
<Home /> <Home />
<About /> <About />
</>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/auth/callback" element={<AuthCallback />} />
</Routes>
</main> </main>
<Footer /> <Footer />
</div> </div>
</BrowserRouter>
); );
} }

View File

@@ -0,0 +1,29 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
export default function AuthCallback() {
const navigate = useNavigate();
useEffect(() => {
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const token = params.get("token");
if (token) {
localStorage.setItem("auth_token", token);
navigate("/");
} else {
navigate("/login?error=no_token");
}
}, [navigate]);
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[hsl(270,73%,63%)] mx-auto mb-4"></div>
<p className="text-gray-400">Completing authentication...</p>
</div>
</div>
);
}

View File

@@ -1,12 +1,131 @@
import { useState } from "react"; import { useState, useEffect } from "react";
interface User {
name?: string;
email?: string;
avatar?: string;
}
export default function Navigation() { export default function Navigation() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const token = localStorage.getItem("auth_token");
if (!token) {
setIsLoading(false);
return;
}
const response = await fetch("/api/v1/auth/session", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setUser(data.user);
console.log("User loaded:", data.user);
} else {
console.error("Token invalid, removing");
localStorage.removeItem("auth_token");
}
} catch (error) {
console.error("Auth check failed:", error);
localStorage.removeItem("auth_token");
} finally {
setIsLoading(false);
}
};
const handleLogout = () => {
localStorage.removeItem("auth_token");
setUser(null);
window.location.href = "/";
};
const getInitials = (user: User): string => {
if (user.name) {
return user.name.substring(0, 2).toUpperCase();
}
if (user.email) {
return user.email.substring(0, 2).toUpperCase();
}
return "?";
};
const AccountAvatar = () => {
if (isLoading) {
return (
<div className="w-10 h-10 rounded-full bg-gray-200 animate-pulse" />
);
}
return ( return (
<> <>
{user ? (
<div
className="relative cursor-pointer group"
onClick={handleLogout}
title={`Logout (${user.name || user.email})`}
>
{user.avatar ? (
<img
src={user.avatar}
alt={user.name || user.email || "User"}
className="w-10 h-10 rounded-full object-cover border-2 border-[hsl(270,73%,63%)] group-hover:border-red-500 transition-colors"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-white font-semibold text-sm group-hover:from-red-500 group-hover:to-red-600 transition-colors">
{getInitials(user)}
</div>
)}
{/* Tooltip при наведении */}
<div className="absolute top-12 right-0 bg-black text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
Click to logout
</div>
</div>
) : (
<a
href="/login"
className="w-10 h-10 rounded-full bg-gray-300 flex items-center justify-center hover:bg-gray-400 transition-colors"
>
<svg
className="w-6 h-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</a>
)}
</>
);
};
return (
<>
{/* Account Avatar - Fixed position, synced with nav */}
<div className="hidden md:block fixed right-4 lg:right-8 top-3 z-50">
<AccountAvatar />
</div>
<nav className="sticky top-0 z-50 py-3"> <nav className="sticky top-0 z-50 py-3">
{/* Desktop */} {/* Desktop Navigation */}
<div className="hidden md:flex gap-8 lg:gap-12 justify-center"> <div className="hidden md:flex gap-8 lg:gap-12 justify-center">
<a <a
href="/" href="/"
@@ -28,10 +147,11 @@ export default function Navigation() {
</a> </a>
</div> </div>
{/* Mobile Burger Button */} {/* Mobile */}
<div className="md:hidden flex justify-between items-center px-4">
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="md:hidden fixed left-4 top-4 z-50 btn btn-ghost btn-sm" className="z-50 btn btn-ghost btn-sm"
> >
<svg <svg
className="w-6 h-6" className="w-6 h-6"
@@ -56,6 +176,8 @@ export default function Navigation() {
)} )}
</svg> </svg>
</button> </button>
<AccountAvatar />
</div>
</nav> </nav>
{/* Mobile Menu */} {/* Mobile Menu */}
@@ -88,6 +210,24 @@ export default function Navigation() {
> >
Guestbook Guestbook
</a> </a>
{user && (
<>
<hr className="my-2" />
<div className="py-3 px-4 text-sm text-gray-600">
{user.name || user.email}
</div>
<button
onClick={() => {
setIsOpen(false);
handleLogout();
}}
className="py-3 px-4 hover:bg-gray-100 rounded-lg transition-all text-red-600 text-left"
>
Logout
</button>
</>
)}
</div> </div>
</div> </div>
</> </>

View File

@@ -0,0 +1,79 @@
export default function Login() {
const handleGitHubLogin = () => {
window.location.href = "/api/v1/auth/github";
};
return (
<div className="flex items-center justify-center px-4 py-12 md:py-0 md:min-h-[calc(100vh-200px)]">
<div className="max-w-md w-full">
{/* ASCII Art Header */}
<pre className="text-center mb-8 text-[10px] sm:text-xs lg:text-sm opacity-80 font-mono leading-tight select-none">
{`
██╗ ██████╗ ██████╗ ██╗███╗ ██╗
██║ ██╔═══██╗██╔════╝ ██║████╗ ██║
██║ ██║ ██║██║ ███╗██║██╔██╗ ██║
██║ ██║ ██║██║ ██║██║██║╚██╗██║
███████╗╚██████╔╝╚██████╔╝██║██║ ╚████║
╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝
╔════════════════════════════════════╗
║ Secure GitHub Authentication ║
╚════════════════════════════════════╝
`}
</pre>
{/* Login Card */}
<div className="border border-gray-700 rounded-lg p-6 sm:p-8 bg-black/30 backdrop-blur-sm shadow-xl">
<h1 className="text-xl sm:text-2xl font-bold mb-2 text-center">
Welcome Back
</h1>
<p className="text-gray-400 text-center mb-6 sm:mb-8 text-sm">
Sign in to continue to your account
</p>
{/* GitHub Login Button */}
<button
onClick={handleGitHubLogin}
className="w-full bg-white text-black hover:bg-gray-200 active:scale-95 transition-all duration-300 py-3 sm:py-4 px-6 rounded-lg font-semibold flex items-center justify-center gap-3 group shadow-lg hover:shadow-xl"
>
<svg
className="w-5 h-5 sm:w-6 sm:h-6 group-hover:scale-110 transition-transform"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm sm:text-base">Login via GitHub</span>
</button>
{/* Divider */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-700"></div>
</div>
<div className="relative flex justify-center text-xs">
<span className="bg-black/30 px-2 text-gray-500">
secure authentication
</span>
</div>
</div>
{/* Footer note */}
<p className="text-gray-500 text-xs text-center">
By signing in, you agree to our{" "}
<a
href="/terms"
className="text-[hsl(270,73%,63%)] hover:underline"
>
terms of service
</a>
</p>
</div>
</div>
</div>
);
}

View File

@@ -5,4 +5,13 @@ import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
secure: false,
},
},
},
}); });