Compare commits

...

49 Commits

Author SHA1 Message Date
d3m0k1d
9094f5e278 feat: update dockerfiles and add .dockerignore 2026-02-28 23:05:29 +03:00
d3m0k1d
bb987b1f8b feat: add logic for repository 2026-02-18 13:43:00 +03:00
d3m0k1d
3fcaefba1b feat: 2026-02-18 13:42:42 +03:00
d3m0k1d
5a375d67c7 feat: fix lint and new nill functions 2026-02-18 13:30:16 +03:00
d3m0k1d
81446e56f5 feat: start create logic for repository 2026-02-18 13:28:27 +03:00
d3m0k1d
e14450f373 fix bug with font on light theme 2026-02-17 16:30:10 +03:00
d3m0k1d
0eca2b1e68 feat: start develop a comment logic 2026-02-15 19:06:17 +03:00
2c3e6578e9 Merge branch 'master' into develop
All checks were successful
Backend ci / build (pull_request) Successful in 3m34s
2026-02-15 15:02:36 +00:00
d3m0k1d
69db666a4b fix: types
All checks were successful
Backend ci / build (pull_request) Successful in 3m40s
2026-02-15 18:02:06 +03:00
a4cffbed23 Merge pull request 'fix: styles for markdown' (#16) from develop into master
Some checks failed
Backend deploy / deploy-backend (push) Successful in 54s
Frontend deploy / deploy-frontend (push) Failing after 59s
Reviewed-on: #16
2026-02-15 14:47:40 +00:00
d4a18b0759 Merge branch 'master' into develop
All checks were successful
Backend ci / build (pull_request) Successful in 3m59s
2026-02-15 14:47:26 +00:00
d3m0k1d
b63e6cc3a8 fix: styles for markdown
All checks were successful
Backend ci / build (pull_request) Successful in 4m2s
2026-02-15 17:46:54 +03:00
55833665a8 Merge pull request 'develop' (#15) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 5m15s
Frontend deploy / deploy-frontend (push) Successful in 2m3s
Reviewed-on: #15
2026-02-15 13:40:08 +00:00
bc4a443f9f Merge branch 'master' into develop
All checks were successful
Backend ci / build (pull_request) Successful in 3m50s
2026-02-15 13:39:37 +00:00
d3m0k1d
78623924ef fix: linter run
All checks were successful
Backend ci / build (pull_request) Successful in 3m57s
2026-02-15 16:37:55 +03:00
d3m0k1d
7f8d8373a9 feat: full redy blog and admin panel 2026-02-15 16:34:37 +03:00
33a41ad066 Merge pull request 'fix redirect to homepage after auth and add static server for files' (#14) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 5m26s
Frontend deploy / deploy-frontend (push) Successful in 1m37s
Reviewed-on: #14
2026-02-15 10:59:33 +00:00
8d6225f136 Merge branch 'master' into develop
Some checks failed
Backend ci / build (pull_request) Failing after 3m31s
2026-02-15 10:33:35 +00:00
d3m0k1d
18b3e318ab feat: update secure on get handler for files
Some checks failed
Backend ci / build (pull_request) Failing after 3m21s
2026-02-15 13:31:28 +03:00
d3m0k1d
482e8571af feat: add static files simple server 2026-02-15 02:15:05 +03:00
d3m0k1d
51f8a125e9 feat: update logic for update query to db for posts 2026-02-15 01:11:59 +03:00
d3m0k1d
a96ef069cc feat: add published at db models and fix repo for this update 2026-02-15 00:51:29 +03:00
d3m0k1d
ea8fa90a31 fix: redirect 2026-02-15 00:22:48 +03:00
bbefe7d28a Merge pull request 'fix: path to api req' (#13) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 54s
Frontend deploy / deploy-frontend (push) Successful in 1m37s
Reviewed-on: #13
2026-02-14 21:16:19 +00:00
74f17c3f3f Merge branch 'master' into develop
Some checks failed
Backend ci / build (pull_request) Has been cancelled
2026-02-14 21:14:40 +00:00
d3m0k1d
0cbed8f5ec fix: path to api req
Some checks failed
Backend ci / build (pull_request) Has been cancelled
2026-02-15 00:14:05 +03:00
38ff90b13f Merge pull request 'fix: bug with avatar fixes' (#12) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 49s
Frontend deploy / deploy-frontend (push) Successful in 1m56s
Reviewed-on: #12
2026-02-14 21:07:12 +00:00
f22a8049f7 Merge branch 'master' into develop
Some checks are pending
Backend ci / build (pull_request) Waiting to run
2026-02-14 21:06:58 +00:00
d3m0k1d
de2735eb16 fix: bug with avatar fixes
Some checks failed
Backend ci / build (pull_request) Has been cancelled
2026-02-15 00:06:25 +03:00
534203b47e Merge pull request 'fix: path to handler' (#11) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 58s
Frontend deploy / deploy-frontend (push) Successful in 1m45s
Reviewed-on: #11
2026-02-14 20:42:41 +00:00
5d8b271da2 Merge branch 'master' into develop
All checks were successful
Backend ci / build (pull_request) Successful in 3m32s
2026-02-14 20:40:24 +00:00
d3m0k1d
af84137f31 fix: path to handler
All checks were successful
Backend ci / build (pull_request) Successful in 3m40s
2026-02-14 23:39:43 +03:00
d145ff537a Merge pull request 'fix: fix db in prod and proxy for backend' (#10) from develop into master
All checks were successful
Backend deploy / deploy-backend (push) Successful in 1m2s
Frontend deploy / deploy-frontend (push) Successful in 1m42s
Reviewed-on: #10
2026-02-14 20:21:34 +00:00
3241413d0d Merge branch 'master' into develop
All checks were successful
Backend ci / build (pull_request) Successful in 3m52s
2026-02-14 20:20:08 +00:00
d3m0k1d
48029ac276 fix: fix db in prod and proxy for backend
All checks were successful
Backend ci / build (pull_request) Successful in 4m7s
2026-02-14 23:18:10 +03:00
6f550882f7 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
2026-02-14 19:40:34 +00:00
1fdfb05f6d Merge branch 'master' into develop
All checks were successful
Backend ci / build (pull_request) Successful in 3m44s
2026-02-14 19:40:24 +00:00
d3m0k1d
fd236962b1 feat: require admin for put del post req
All checks were successful
Backend ci / build (pull_request) Successful in 3m40s
2026-02-14 22:33:51 +03:00
d3m0k1d
8b7b732a24 feat: change auth type from cookies to localstorage add loadpage, test auth to prod 2026-02-14 22:29:55 +03:00
d3m0k1d
2b794f97a3 feat: add validator for jwt 2026-02-14 21:51:44 +03:00
d3m0k1d
4712604171 feat: add login page 2026-02-14 16:56:43 +03:00
d3m0k1d
7e4dbd7e56 feat: add auth to swagerr add middleware for check admin 2026-02-14 16:26:27 +03:00
d3m0k1d
05a8f0cbc7 fix: add return after failure req 2026-02-14 15:42:43 +03:00
d3m0k1d
9ec570688c feat: update docs models responses 2026-02-14 15:35:25 +03:00
d3m0k1d
3bf3e2a233 feat: add simple docker compose file, change pipline, add docker build command to makefile, update db path logic in storage packages, minor fixes in handlers 2026-02-14 14:40:42 +03:00
d3m0k1d
c2d5178b33 feat: update deps and add test file
All checks were successful
Backend ci / build (push) Successful in 3m33s
2026-02-13 15:05:14 +03:00
d3m0k1d
2c198e56eb feat: update handlers in posts
All checks were successful
Backend ci / build (push) Successful in 3m29s
2026-02-13 14:46:44 +03:00
d3m0k1d
15cf6cca8a del fake db
All checks were successful
Backend ci / build (push) Successful in 3m44s
2026-02-12 23:58:56 +03:00
d3m0k1d
d9cf0ade7f feat: update docs, fix repository, full working github callback, fix healthcheck in docker file, update makefile 2026-02-12 23:58:56 +03:00
45 changed files with 5638 additions and 301 deletions

View File

@@ -44,10 +44,14 @@ jobs:
username: ${{ steps.import-secrets.outputs.SERVER_USER }} username: ${{ steps.import-secrets.outputs.SERVER_USER }}
key: ${{ steps.import-secrets.outputs.SSH_KEY }} key: ${{ steps.import-secrets.outputs.SSH_KEY }}
script: | script: |
mkdir -p /opt/d3m0k1d/data
docker login -u d3m0k1d -p ${{ steps.import-secrets.outputs.GITEA_TOKEN }} gitea.d3m0k1d.ru docker login -u d3m0k1d -p ${{ steps.import-secrets.outputs.GITEA_TOKEN }} gitea.d3m0k1d.ru
docker pull gitea.d3m0k1d.ru/d3m0k1d/backend:latest docker pull gitea.d3m0k1d.ru/d3m0k1d/backend:latest
docker rm -f d3m0k1d-backend || true docker rm -f d3m0k1d-backend || true
docker run --name d3m0k1d-backend -d -p 8080:8080 \ docker run --name d3m0k1d-backend -d -p 8080:8080 \
--network d3m0k1d-network \
-v /opt/d3m0k1d/data:/data \
-e DB_PATH="/data/d3m0k1d.db" \
-e JWT_SECRET="${{ steps.import-secrets.outputs.JWT_SECRET }}" \ -e JWT_SECRET="${{ steps.import-secrets.outputs.JWT_SECRET }}" \
-e GITHUB_CLIENT_ID="${{ steps.import-secrets.outputs.GITHUB_CLIENT_ID }}" \ -e GITHUB_CLIENT_ID="${{ steps.import-secrets.outputs.GITHUB_CLIENT_ID }}" \
-e GITHUB_CLIENT_SECRET="${{ steps.import-secrets.outputs.GITHUB_CLIENT_SECRET }}" \ -e GITHUB_CLIENT_SECRET="${{ steps.import-secrets.outputs.GITHUB_CLIENT_SECRET }}" \

View File

@@ -44,4 +44,4 @@ jobs:
docker login -u d3m0k1d -p ${{ steps.import-secrets.outputs.GITEA_TOKEN }} gitea.d3m0k1d.ru docker login -u d3m0k1d -p ${{ steps.import-secrets.outputs.GITEA_TOKEN }} gitea.d3m0k1d.ru
docker pull gitea.d3m0k1d.ru/d3m0k1d/frontend:latest docker pull gitea.d3m0k1d.ru/d3m0k1d/frontend:latest
docker rm -f d3m0k1d-frontend || true docker rm -f d3m0k1d-frontend || true
docker run --name d3m0k1d-frontend -d -p 80:80 --restart unless-stopped gitea.d3m0k1d.ru/d3m0k1d/frontend:latest docker run --name d3m0k1d-frontend -d -p 80:80 --restart unless-stopped --network d3m0k1d-network gitea.d3m0k1d.ru/d3m0k1d/frontend:latest

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

6
backend/.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
docker-compose.yml
data/
Makefile
.env
docs/

View File

@@ -3,17 +3,21 @@ FROM golang:1.25.6 AS builder
WORKDIR /app WORKDIR /app
COPY . .
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
ENV GIN_MODE=release ENV GIN_MODE=release
RUN go mod tidy
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" -o backend ./cmd/main.go RUN go build -ldflags "-s -w" -o backend ./cmd/main.go
FROM alpine:3.23.0 FROM alpine:3.23.0
RUN adduser -D appuser && apk add --no-cache curl
COPY --from=builder /app/backend . COPY --from=builder /app/backend .
RUN chown appuser:appuser ./backend
USER appuser
EXPOSE 8080 EXPOSE 8080
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 docker-run
test: test:
@@ -19,3 +19,19 @@ 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 .
docker-run:
docker run --rm -p 8080:8080 --env-file .env --network host -v /opt/d3m0k1d.ru/data:/data backend:latest
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,9 +15,14 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/callback/github": { "/admin/posts": {
"get": { "get": {
"description": "Callback for oauth2 providers", "security": [
{
"Bearer": []
}
],
"description": "Get all posts",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -25,12 +30,104 @@ const docTemplate = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"auth" "admin"
], ],
"summary": "Callback for oauth2 providers", "summary": "Get all posts",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
}
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"404": {
"description": "No Post found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
}
}
}
},
"/auth/github": {
"get": {
"description": "Redirects to GitHub authorization",
"tags": [
"auth"
],
"summary": "Start GitHub OAuth login",
"responses": {
"302": {
"description": "Found"
}
}
}
},
"/callback/github": {
"get": {
"description": "Exchanges authorization code for access token",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "GitHub OAuth callback",
"parameters": [
{
"type": "string",
"description": "Authorization code",
"name": "code",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"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": {
@@ -53,20 +150,55 @@ const docTemplate = `{
"tags": [ "tags": [
"posts" "posts"
], ],
"summary": "Get all posts", "summary": "Get all published posts",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array", "allOf": [
"items": { {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
} },
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
}
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"404": {
"description": "No Post found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
} }
} }
}, },
"post": { "post": {
"security": [
{
"Bearer": []
}
],
"description": "Create new post", "description": "Create new post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -93,13 +225,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 +275,47 @@ 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.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 +349,41 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" "allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate"
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
} }
} }
}, },
"delete": { "delete": {
"security": [
{
"Bearer": []
}
],
"description": "Delete post", "description": "Delete post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -205,11 +395,162 @@ 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": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" "allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post"
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"404": {
"description": "Post not found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
}
}
}
},
"/session": {
"get": {
"description": "Returns user session data",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Get user session",
"responses": {
"200": {
"description": "Session data",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/upload": {
"post": {
"description": "Upload static content to the server",
"produces": [
"application/json"
],
"tags": [
"static"
],
"summary": "Upload static content",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
}
}
}
},
"/upload/{file}": {
"get": {
"description": "Get static content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"static"
],
"summary": "Get static content",
"parameters": [
{
"type": "string",
"description": "File name",
"name": "file",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Static content",
"schema": {
"allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
},
"404": {
"description": "File 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"
} }
} }
} }
@@ -217,9 +558,33 @@ const docTemplate = `{
} }
}, },
"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,12 +592,15 @@ const docTemplate = `{
"content": { "content": {
"type": "string" "type": "string"
}, },
"createdAt": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -244,6 +612,9 @@ const docTemplate = `{
"content": { "content": {
"type": "string" "type": "string"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -255,6 +626,9 @@ const docTemplate = `{
"content": { "content": {
"type": "string" "type": "string"
}, },
"created_at": {
"type": "string"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@@ -263,6 +637,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,9 +4,14 @@
"contact": {} "contact": {}
}, },
"paths": { "paths": {
"/callback/github": { "/admin/posts": {
"get": { "get": {
"description": "Callback for oauth2 providers", "security": [
{
"Bearer": []
}
],
"description": "Get all posts",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -14,12 +19,104 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"auth" "admin"
], ],
"summary": "Callback for oauth2 providers", "summary": "Get all posts",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
}
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"404": {
"description": "No Post found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
}
}
}
},
"/auth/github": {
"get": {
"description": "Redirects to GitHub authorization",
"tags": [
"auth"
],
"summary": "Start GitHub OAuth login",
"responses": {
"302": {
"description": "Found"
}
}
}
},
"/callback/github": {
"get": {
"description": "Exchanges authorization code for access token",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "GitHub OAuth callback",
"parameters": [
{
"type": "string",
"description": "Authorization code",
"name": "code",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"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": {
@@ -42,20 +139,55 @@
"tags": [ "tags": [
"posts" "posts"
], ],
"summary": "Get all posts", "summary": "Get all published posts",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array", "allOf": [
"items": { {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
} },
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
}
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"404": {
"description": "No Post found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
} }
} }
}, },
"post": { "post": {
"security": [
{
"Bearer": []
}
],
"description": "Create new post", "description": "Create new post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -82,13 +214,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 +264,47 @@
"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.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 +338,41 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate" "allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate"
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
} }
} }
} }
}, },
"delete": { "delete": {
"security": [
{
"Bearer": []
}
],
"description": "Delete post", "description": "Delete post",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -194,11 +384,162 @@
"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": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post" "allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post"
}
}
}
]
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"404": {
"description": "Post not found",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
}
}
}
},
"/session": {
"get": {
"description": "Returns user session data",
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Get user session",
"responses": {
"200": {
"description": "Session data",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/upload": {
"post": {
"description": "Upload static content to the server",
"produces": [
"application/json"
],
"tags": [
"static"
],
"summary": "Upload static content",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse"
}
}
}
}
},
"/upload/{file}": {
"get": {
"description": "Get static content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"static"
],
"summary": "Get static content",
"parameters": [
{
"type": "string",
"description": "File name",
"name": "file",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Static content",
"schema": {
"allOf": [
{
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
},
"404": {
"description": "File 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"
} }
} }
} }
@@ -206,9 +547,33 @@
} }
}, },
"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,12 +581,15 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"createdAt": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -233,6 +601,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"published": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
} }
@@ -244,6 +615,9 @@
"content": { "content": {
"type": "string" "type": "string"
}, },
"created_at": {
"type": "string"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@@ -252,5 +626,13 @@
} }
} }
} }
},
"securityDefinitions": {
"Bearer": {
"description": "Type \"Bearer\" followed by a space and the JWT token.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
} }
} }

View File

@@ -1,15 +1,32 @@
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
published:
type: boolean
title: title:
type: string type: string
type: object type: object
@@ -17,6 +34,8 @@ definitions:
properties: properties:
content: content:
type: string type: string
published:
type: boolean
title: title:
type: string type: string
type: object type: object
@@ -24,6 +43,8 @@ definitions:
properties: properties:
content: content:
type: string type: string
created_at:
type: string
id: id:
type: integer type: integer
title: title:
@@ -32,21 +53,81 @@ definitions:
info: info:
contact: {} contact: {}
paths: paths:
/callback/github: /admin/posts:
get: get:
consumes: consumes:
- application/json - application/json
description: Callback for oauth2 providers description: Get all posts
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema:
allOf:
- $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
- properties:
data:
items:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
type: array
type: object
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"404":
description: No Post found
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
security:
- Bearer: []
summary: Get all posts
tags:
- admin
/auth/github:
get:
description: Redirects to GitHub authorization
responses:
"302":
description: Found
summary: Start GitHub OAuth login
tags:
- auth
/callback/github:
get:
description: Exchanges authorization code for access token
parameters:
- description: Authorization code
in: query
name: code
required: true
type: string
produces:
- application/json
responses:
"200":
description: 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,10 +141,27 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
items: allOf:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq' - $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
type: array - properties:
summary: Get all posts data:
items:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
type: array
type: object
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"404":
description: No Post found
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
summary: Get all published posts
tags: tags:
- posts - posts
post: post:
@@ -83,11 +181,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 +201,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:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post' allOf:
- $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
- properties:
data:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post'
type: object
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"404":
description: Post not found
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
security:
- Bearer: []
summary: Delete post summary: Delete post
tags: tags:
- posts - posts
@@ -122,19 +252,24 @@ 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.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 +295,100 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate' allOf:
- $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
- properties:
data:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate'
type: object
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
security:
- Bearer: []
summary: Update post 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
/upload:
post:
description: Upload static content to the server
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.ErrorResponse'
summary: Upload static content
tags:
- static
/upload/{file}:
get:
consumes:
- application/json
description: Get static content
parameters:
- description: File name
in: path
name: file
required: true
type: string
produces:
- application/json
responses:
"200":
description: Static content
schema:
allOf:
- $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_models.SuccessResponse'
- properties:
data:
type: string
type: object
"404":
description: File 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'
summary: Get static content
tags:
- static
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"
@@ -14,9 +15,13 @@ var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
func GenerateJWT(user storage.User) (string, error) { 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,
"github_id": user.GithubID, "login": user.GithubLogin,
"github_id": user.GithubID,
"avatar_url": user.AvatarURL,
"exp": time.Now().Add(30 * 24 * time.Hour).Unix(), // 30 дней
"iat": time.Now().Unix(),
}) })
tokenString, err := token.SignedString(jwtSecret) 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

@@ -3,6 +3,7 @@ package handlers
import ( 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"
@@ -14,15 +15,17 @@ import (
) )
type AuthHandlers struct { type AuthHandlers struct {
repo repositories.AuthRepository repo repositories.AuthRepository
logger *logger.Logger logger *logger.Logger
config *oauth2.Config config *oauth2.Config
frontendURL string
} }
func NewAuthHandlers(repo repositories.AuthRepository) *AuthHandlers { func NewAuthHandlers(repo repositories.AuthRepository) *AuthHandlers {
clientID := os.Getenv("GITHUB_CLIENT_ID") clientID := os.Getenv("GITHUB_CLIENT_ID")
clientSecret := os.Getenv("GITHUB_CLIENT_SECRET") clientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
redirectURL := os.Getenv("REDIRECT_URL") redirectURL := os.Getenv("REDIRECT_URL")
frontendURL := os.Getenv("FRONTEND_URL")
if clientID == "" || clientSecret == "" { if clientID == "" || clientSecret == "" {
panic("GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET must be set") panic("GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET must be set")
@@ -30,10 +33,14 @@ func NewAuthHandlers(repo repositories.AuthRepository) *AuthHandlers {
if redirectURL == "" { if redirectURL == "" {
redirectURL = "http://localhost:8080/api/v1/callback/github" redirectURL = "http://localhost:8080/api/v1/callback/github"
} }
if frontendURL == "" {
frontendURL = "https://d3m0k1d.ru"
}
return &AuthHandlers{ return &AuthHandlers{
repo: repo, repo: repo,
logger: logger.New(false), logger: logger.New(false),
frontendURL: frontendURL,
config: &oauth2.Config{ config: &oauth2.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: clientSecret, ClientSecret: clientSecret,
@@ -49,7 +56,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 +80,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, h.frontendURL+"/login?error=missing_code")
return return
} }
@@ -82,7 +89,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, h.frontendURL+"/login?error=auth_failed")
return return
} }
@@ -90,7 +97,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, h.frontendURL+"/login?error=github_api_failed")
return return
} }
@@ -98,14 +105,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, h.frontendURL+"/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, h.frontendURL+"/login?error=database_error")
return return
} }
@@ -114,10 +121,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, h.frontendURL+"/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, h.frontendURL+"/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 +145,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, h.frontendURL+"/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, h.frontendURL+"/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

@@ -0,0 +1,72 @@
package handlers
import (
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"github.com/gin-gonic/gin"
)
type CommentsHandlers struct {
logger *logger.Logger
}
func NewCommentsHandlers() *CommentsHandlers {
return &CommentsHandlers{logger: logger.New(false)}
}
// GetAllComments godoc
// @Summary Get all comments
// @Description Get all comments
// @Tags comments
// @Accept json
// @Produce json
// @Success 200 {object} models.SuccessResponse{data=[]storage.Comment}
// @Failure 404 {object} models.ErrorResponse "No Comment found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /comments [get]
func (h *CommentsHandlers) GetAllComments(c *gin.Context) {
}
// GetCommentsOfPost godoc
// @Summary Get comments of post
// @Description Get comments of post
// @Tags comments
// @Accept json
// @Produce json
// @Param id path int true "Post ID"
// @Success 200 {object} models.SuccessResponse{data=[]storage.Comment}
// @Failure 404 {object} models.ErrorResponse "No Comment found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /posts/{id}/comments [get]
func (h *CommentsHandlers) GetCommentsOfPost(c *gin.Context) {
}
// CreateComment godoc
// @Summary Create comment
// @Description Create new comment
// @Tags comments
// @Accept json
// @Produce json
// @Param comment body storage.CommentCreate true "Comment data"
// @Security Bearer
// @Success 200 {object} models.SuccessResponse{data=storage.Comment}
// @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /comments [post]
func (h *CommentsHandlers) CreateComment(c *gin.Context) {
}
// DeleteComment godoc
// @Summary Delete comment
// @Description Delete comment
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param id path int true "Comment ID"
// @Success 200 {object} models.SuccessResponse{data=storage.Comment}
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Comment not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router admin/comments/{id} [delete]
func (h *CommentsHandlers) DeleteComment(c *gin.Context) {
}

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"
@@ -19,22 +20,30 @@ func NewPostHandlers(repo repositories.PostRepository) *PostHandlers {
} }
// GetPosts godoc // GetPosts godoc
// @Summary Get all posts // @Summary Get all published posts
// @Description Get all posts // @Description Get all posts
// @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,28 +95,30 @@ 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
} }
post := storage.Post{ post := storage.Post{
Title: req.Title, Title: req.Title,
Content: req.Content, Content: req.Content,
Tags: req.Tags,
} }
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 +129,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 +162,63 @@ 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")
}
// GetPosts godoc
// @Summary Get all posts
// @Description Get all posts
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @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 /admin/posts [get]
func (h *PostHandlers) AdminGetAll(c *gin.Context) {
var result []storage.PostReq
result, err := h.repo.GetAllAdmin(c.Request.Context())
if err != nil {
h.logger.Error("error request: " + err.Error())
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")
models.Success(c, result)
} }

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"
) )
@@ -10,16 +11,26 @@ import (
func Register(router *gin.Engine, db *sql.DB) { func Register(router *gin.Engine, db *sql.DB) {
handler_posts := NewPostHandlers(repositories.NewPostRepository(db)) handler_posts := NewPostHandlers(repositories.NewPostRepository(db))
handler_auth := NewAuthHandlers(repositories.NewAuthRepository(db)) handler_auth := NewAuthHandlers(repositories.NewAuthRepository(db))
handler_static := NewStaticHandlers()
router.GET("/health", func(c *gin.Context) { c.Status(200) }) router.GET("/health", func(c *gin.Context) { c.Status(200) })
v1 := router.Group("api/v1") v1 := router.Group("api/v1")
admin := v1.Group("admin")
{
admin.GET("/posts", auth.JWTMiddleware(), auth.RequireAdmin(), handler_posts.AdminGetAll)
}
v1.Static("/uploads", "/data/uploads")
v1.POST("/upload", auth.JWTMiddleware(), auth.RequireAdmin(), handler_static.PostStatic)
v1.GET("/upload/:file", handler_static.GetStatic)
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,91 @@
package handlers
import (
"os"
"path/filepath"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/models"
"github.com/gin-gonic/gin"
)
type StaticHandlers struct {
logger *logger.Logger
}
func NewStaticHandlers() *StaticHandlers {
return &StaticHandlers{
logger: logger.New(false),
}
}
// PostStatic godoc
// @Summary Upload static content
// @Description Upload static content to the server
// @Tags static
// @Produce json
// @Success 200 {object} models.SuccessResponse(data=string) "Static content"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /upload [post]
func (h *StaticHandlers) PostStatic(c *gin.Context) {
content, err := c.FormFile("file")
if err != nil {
h.logger.Error("error request: " + err.Error())
models.Error(c, 500, "Internal server error", err.Error())
return
}
dst := "/data/upload/" + content.Filename
if err = c.SaveUploadedFile(content, dst); err != nil {
h.logger.Error("error request: " + err.Error())
models.Error(c, 500, "Internal server error", err.Error())
return
}
models.Success(c, "Static content saved")
}
// GetStatic godoc
// @Summary Get static content
// @Description Get static content
// @Tags static
// @Accept json
// @Produce json
// @Param file path string true "File name"
// @Success 200 {object} models.SuccessResponse{data=string} "Static content"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Failure 404 {object} models.ErrorResponse "File not found"
// @Router /upload/{file} [get]
func (h *StaticHandlers) GetStatic(c *gin.Context) {
filename := c.Param("file")
if filename == "" {
models.Error(c, 404, "File not found", "")
return
}
filename = filepath.Clean(filename)
if strings.Contains(filename, "..") {
models.Error(c, 400, "Invalid file path", "")
return
}
if filepath.IsAbs(filename) {
models.Error(c, 400, "Invalid file path", "")
return
}
baseDir := "/data/upload/"
fullPath := filepath.Join(baseDir, filename)
if !strings.HasPrefix(fullPath, baseDir) {
models.Error(c, 400, "Invalid file path", "")
return
}
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
models.Error(c, 404, "File not found", "")
return
}
c.File(fullPath)
}

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, err
} }
return false, nil return true, nil
}
func (r *authRepository) GetUserByGithubID(
ctx context.Context,
githubID int,
) (*storage.User, error) {
var user storage.User
query := `SELECT id, github_id, github_login, email, avatar_url FROM users WHERE github_id = ?`
err := r.db.QueryRowContext(ctx, query, githubID).Scan(
&user.ID,
&user.GithubID,
&user.GithubLogin,
&user.Email,
&user.AvatarURL,
)
if err != nil {
return nil, err
}
return &user, nil
} }

View File

@@ -0,0 +1,86 @@
package repositories
import (
"context"
"database/sql"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
)
type commentsRepository struct {
db *sql.DB
logger *logger.Logger
}
func NewCommentsRepository(db *sql.DB) CommentRepository {
return &commentsRepository{
db: db,
logger: logger.New(false),
}
}
func (c *commentsRepository) CreateComment(
ctx context.Context,
comment *storage.CommentCreate,
) error {
_, err := c.db.Exec(
"INSERT INTO comments(content, post_id) VALUES(?, ?, ?)",
comment.Content,
comment.PostID,
comment.UserID,
)
if err != nil {
c.logger.Error("error insert: " + err.Error())
return err
}
return nil
}
func (c *commentsRepository) GetAllComments(ctx context.Context) ([]storage.Comment, error) {
var result []storage.Comment
rows, err := c.db.Query("SELECT id, content, post_id, user_id, created_at FROM comments")
if err != nil {
c.logger.Error("error scan " + err.Error())
return nil, err
}
for rows.Next() {
var id int
var content string
var postID int
var userID int
var createdAt string
err := rows.Scan(&id, &content, &postID, &userID, &createdAt)
if err != nil {
c.logger.Error("error scan: " + err.Error())
}
result = append(result, storage.Comment{
ID: id,
Content: content,
PostID: postID,
UserID: userID,
CreatedAt: createdAt,
})
}
return result, nil
}
func (c *commentsRepository) GetCommentsOfPost(
ctx context.Context,
id int,
) ([]storage.Comment, error) {
return nil, nil
}
func (c *commentsRepository) DeleteComment(ctx context.Context, id int) error {
return nil
}
func (c *commentsRepository) UpdateComment(
ctx context.Context,
id int,
comment *storage.Comment,
) error {
return nil
}

View File

@@ -8,7 +8,10 @@ import (
type PostRepository interface { type PostRepository interface {
GetAll(ctx context.Context) ([]storage.PostReq, error) GetAll(ctx context.Context) ([]storage.PostReq, error)
GetAllAdmin(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 +20,13 @@ 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)
}
type CommentRepository interface {
CreateComment(ctx context.Context, comment *storage.CommentCreate) error
GetAllComments(ctx context.Context) ([]storage.Comment, error)
GetCommentsOfPost(ctx context.Context, id int) ([]storage.Comment, error)
DeleteComment(ctx context.Context, id int) error
UpdateComment(ctx context.Context, id int, comment *storage.Comment) error
} }

View File

@@ -3,6 +3,7 @@ package repositories
import ( import (
"context" "context"
"database/sql" "database/sql"
"strings"
"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/storage" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
@@ -21,7 +22,7 @@ func NewPostRepository(db *sql.DB) PostRepository {
} }
func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) { func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error) {
var result []storage.PostReq var result []storage.PostReq
rows, err := p.db.Query("SELECT id, title, content FROM posts") rows, err := p.db.Query("SELECT id, title, content, CREATED_AT FROM posts WHERE published = 1")
if err != nil { if err != nil {
p.logger.Error(err.Error()) p.logger.Error(err.Error())
return nil, err return nil, err
@@ -30,14 +31,16 @@ func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error)
var title string var title string
var content string var content string
var id int var id int
err := rows.Scan(&id, &title, &content) var createdAt string
err := rows.Scan(&id, &title, &content, &createdAt)
if err != nil { if err != nil {
p.logger.Error("error scan: " + err.Error()) p.logger.Error("error scan: " + err.Error())
} }
result = append(result, storage.PostReq{ result = append(result, storage.PostReq{
ID: id, ID: id,
Title: title, Title: title,
Content: content, Content: content,
CreatedAt: createdAt,
}) })
} }
return result, nil return result, nil
@@ -45,26 +48,32 @@ func (p *postRepository) GetAll(ctx context.Context) ([]storage.PostReq, error)
func (p *postRepository) GetByID(ctx context.Context, id int) (storage.PostReq, error) { func (p *postRepository) GetByID(ctx context.Context, id int) (storage.PostReq, error) {
var result storage.PostReq var result storage.PostReq
row := p.db.QueryRow("SELECT title, content FROM posts WHERE id = ?", id) row := p.db.QueryRow(
"SELECT title, content, CREATED_AT FROM posts WHERE id = ? AND published = 1",
id,
)
var title string var title string
var content string var content string
err := row.Scan(&title, &content) var createdAt string
err := row.Scan(&title, &content, &createdAt)
if err != nil { if err != nil {
p.logger.Error("error scan: " + err.Error()) p.logger.Error("error scan: " + err.Error())
} }
result = storage.PostReq{ result = storage.PostReq{
ID: id, ID: id,
Title: title, Title: title,
Content: content, Content: content,
CreatedAt: createdAt,
} }
return result, nil return result, nil
} }
func (p *postRepository) Create(ctx context.Context, post storage.Post) error { func (p *postRepository) Create(ctx context.Context, post storage.Post) error {
query, err := p.db.Exec( query, err := p.db.Exec(
"INSERT INTO posts(title, content) VALUES(?, ?)", "INSERT INTO posts(title, content) VALUES(?, ?, ?)",
post.Title, post.Title,
post.Content, post.Content,
post.Tags,
) )
if err != nil { if err != nil {
return err return err
@@ -78,20 +87,90 @@ func (p *postRepository) Create(ctx context.Context, post storage.Post) error {
} }
func (p *postRepository) Update(ctx context.Context, id int, post storage.Post) error { func (p *postRepository) Update(ctx context.Context, id int, post storage.Post) error {
_, err := p.db.Exec( query := "UPDATE posts SET "
"UPDATE posts SET title = ?, content = ? WHERE id = ?", args := []interface{}{}
post.Title, updates := []string{}
post.Content,
id, if post.Title != "" {
) updates = append(updates, "title = ?")
if err != nil { args = append(args, post.Title)
return err
} }
p.logger.Info("Post updated:", "id", id) if post.Tags != "" {
return nil updates = append(updates, "tags = ?")
args = append(args, post.Tags)
}
if post.Content != "" {
updates = append(updates, "content = ?")
args = append(args, post.Content)
}
updates = append(updates, "published = ?")
args = append(args, post.Published)
updates = append(updates, "updated_at = CURRENT_TIMESTAMP")
query += strings.Join(updates, ", ")
query += " WHERE id = ?"
args = append(args, id)
_, err := p.db.ExecContext(ctx, query, args...)
return err
} }
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
}
func (p *postRepository) GetAllAdmin(ctx context.Context) ([]storage.PostReq, error) {
result := []storage.PostReq{}
rows, err := p.db.Query("SELECT id, title, content, tags, CREATED_AT FROM posts")
if err != nil {
p.logger.Error(err.Error())
return nil, err
}
for rows.Next() {
var title string
var content string
var id int
var tags string
var createdAt string
err := rows.Scan(&id, &title, &content, &tags, &createdAt)
if err != nil {
p.logger.Error("error scan: " + err.Error())
}
result = append(result, storage.PostReq{
ID: id,
Title: title,
Content: content,
Tags: tags,
CreatedAt: createdAt,
})
}
return result, nil
}

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

@@ -4,8 +4,11 @@ const Migrations = `
CREATE TABLE IF NOT EXISTS posts( CREATE TABLE IF NOT EXISTS posts(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL, title TEXT NOT NULL,
published BOOLEAN DEFAULT 0,
content TEXT NOT NULL, content TEXT NOT NULL,
CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
tags TEXT
); );
CREATE TABLE IF NOT EXISTS users( CREATE TABLE IF NOT EXISTS users(
@@ -15,4 +18,14 @@ CREATE TABLE IF NOT EXISTS users(
github_login TEXT, github_login TEXT,
avatar_url TEXT avatar_url TEXT
); );
CREATE TABLE IF NOT EXISTS comments(
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
user_id INTEGER,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
` `

View File

@@ -1,23 +1,31 @@
package storage package storage
// Post
type Post struct { type Post struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"` Title string `db:"title" json:"title"`
Published bool `db:"published" json:"published"`
Content string `db:"content" json:"content"` Content string `db:"content" json:"content"`
CreatedAt string `db:"created_at" json:"created_at"` CreatedAt string `db:"created_at" json:"created_at"`
Tags string `db:"tags" json:"tags"`
} }
type PostReq struct { type PostReq struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"` Title string `db:"title" json:"title"`
Content string `db:"content" json:"content"` Content string `db:"content" json:"content"`
CreatedAt string `db:"created" json:"created_at"`
Tags string `db:"tags" json:"tags"`
} }
type PostCreate struct { type PostCreate struct {
Title string `db:"title" json:"title"` Title string `db:"title" json:"title"`
Content string `db:"content" json:"content"` Published bool `db:"published" json:"published"`
Content string `db:"content" json:"content"`
Tags string `db:"tags" json:"tags"`
} }
// User
type User struct { type User struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Email string `db:"email" json:"email"` Email string `db:"email" json:"email"`
@@ -27,8 +35,31 @@ 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"`
}
// Comment
type Comment struct {
ID int `db:"id" json:"id"`
PostID int `db:"post_id" json:"post_id"`
UserID int `db:"user_id" json:"user_id"`
Content string `db:"content" json:"content"`
CreatedAt string `db:"created_at" json:"created_at"`
}
type CommentReq struct {
ID int `db:"id" json:"id"`
PostID int `db:"post_id" json:"post_id"`
UserID int `db:"user_id" json:"user_id"`
Content string `db:"content" json:"content"`
CreatedAt string `db:"created_at" json:"created_at"`
}
type CommentCreate struct {
PostID int `db:"post_id" json:"post_id"`
UserID int `db:"user_id" json:"user_id"`
Content string `db:"content" json:"content"`
} }

4
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
Makefile
dist
node_modules

View File

@@ -1,42 +1,117 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="title"
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">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "d3m0k1d",
"url": "https://d3m0k1d.ru",
"jobTitle": "DevOps Engineer",
"description": "DevOps Engineer, InfoSec student at DSTU, and Go backend developer",
"alumniOf": {
"@type": "EducationalOrganization",
"name": "Don State Technical University"
},
"knowsAbout": [
"DevOps",
"Information Security",
"Backend Development",
"Go",
"Linux",
"Infrastructure Automation"
],
"sameAs": ["https://github.com/d3m0k1d"]
}
</script>
<link rel="canonical" href="https://d3m0k1d.ru" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="author" content="d3m0k1d" />
<meta name="robots" content="index, follow" />
<title>d3m0k1d - DevOps Engineer & InfoSec Student</title>
<head> <style>
<meta charset="UTF-8" /> #initial-loader {
<link rel="icon" type="image/png" href="/favicon.png" /> position: fixed;
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> inset: 0;
<meta name="title" content="d3m0k1d - DevOps Engineer & InfoSec Student | Go Backend Developer" /> background: #000;
<meta name="keywords" display: flex;
content="DevOps, InfoSec, Backend Developer, Go, Linux, Security, Portfolio, Programming, Personal Website, Personal blog, DSTU, Don State Technical Unversity, Unix" /> flex-direction: column;
<script type="application/ld+json"> align-items: center;
{ justify-content: center;
"@context": "https://schema.org", z-index: 9999;
"@type": "Person", font-family: monospace;
"name": "d3m0k1d", }
"url": "https://d3m0k1d.ru",
"jobTitle": "DevOps Engineer",
"description": "DevOps Engineer, InfoSec student at DSTU, and Go backend developer",
"alumniOf": {
"@type": "EducationalOrganization",
"name": "Don State Technical University"
},
"knowsAbout": ["DevOps", "Information Security", "Backend Development", "Go", "Linux", "Infrastructure Automation"],
"sameAs": [
"https://github.com/d3m0k1d",
] #initial-loader .spinner {
} width: 48px;
</script> height: 48px;
<link rel="canonical" href="https://d3m0k1d.ru" /> border: 3px solid rgba(255, 255, 255, 0.1);
<link rel="icon" type="image/svg+xml" href="/favicon.png" /> border-top-color: hsl(270, 73%, 63%);
<meta name="author" content="d3m0k1d" /> border-radius: 50%;
<meta name="robots" content="index, follow" /> animation: spin 1s linear infinite;
<title>d3m0k1d - DevOps Engineer & InfoSec Student</title> }
</head>
<body> #initial-loader .text {
<div id="root"></div> margin-top: 16px;
<script type="module" src="/src/main.tsx"></script> color: #666;
</body> 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>
<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>
<script type="module" src="/src/main.tsx"></script>
</body>
</html> </html>

View File

@@ -8,8 +8,19 @@ server {
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
location /api/ {
proxy_pass http://d3m0k1d-backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Кэширование статики
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,22 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"prismjs": "^1.30.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"react-syntax-highlighter": "^16.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.5", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"daisyui": "^5.5.14", "daisyui": "^5.5.14",
"eslint": "^9.39.1", "eslint": "^9.39.1",

View File

@@ -1,7 +1,16 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui"; @plugin "daisyui";
#root { #root {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
color: oklch(var(--bc));
}
body {
color: oklch(var(--bc));
background-color: oklch(var(--b1));
} }

View File

@@ -1,18 +1,53 @@
// frontend/src/App.tsx
import "./App.css"; import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useEffect } from "react";
import { AuthProvider } from "./contexts/AuthContext.tsx";
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";
import Admin from "./pages/Admin.tsx";
import Upload from "./pages/Upload.tsx";
import Blog from "./pages/Blog.tsx";
import BlogPost from "./pages/BlogPost.tsx";
function App() { function App() {
useEffect(() => {
document.body.classList.add("loaded");
}, []);
return ( return (
<div className="min-h-screen flex flex-col"> <AuthProvider>
<Navigation /> <BrowserRouter>
<main className="flex-grow"> <div className="min-h-screen flex flex-col">
<Home /> <Navigation />
<About /> <main className="flex-grow">
</main> <Routes>
<Footer /> <Route
</div> path="/"
element={
<>
<Home />
<About />
</>
}
/>
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:id" element={<BlogPost />} />{" "}
{/* Новый роут */}
<Route path="/login" element={<Login />} />
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/admin" element={<Admin />} />
<Route path="/admin/upload" element={<Upload />} />
</Routes>
</main>
<Footer />
</div>
</BrowserRouter>
</AuthProvider>
); );
} }

View File

@@ -0,0 +1,36 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext.tsx";
export default function AuthCallback() {
const navigate = useNavigate();
const { checkAuth } = useAuth();
useEffect(() => {
const processAuth = async () => {
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const token = params.get("token");
if (token) {
localStorage.setItem("auth_token", token);
console.log("Token saved, loading user...");
await checkAuth();
window.location.href = "/";
} else {
console.error("No token in URL");
navigate("/login?error=no_token");
}
};
processAuth();
}, [navigate, checkAuth]);
return (
<div className="fixed inset-0 bg-black flex items-center justify-center">
<div className="w-12 h-12 border-2 border-gray-800 border-t-[hsl(270,73%,63%)] rounded-full animate-spin"></div>
</div>
);
}

View File

@@ -1,12 +1,91 @@
import { useState } from "react"; import { useState } from "react";
import { useAuth } from "../contexts/AuthContext.tsx";
export default function Navigation() { function AccountAvatar() {
const [isOpen, setIsOpen] = useState(false); const { user, isLoading, logout } = useAuth();
const handleLogout = () => {
logout();
window.location.href = "/";
};
const getInitials = (user: { name?: string; email?: string }): string => {
if (user.name) {
return user.name.substring(0, 2).toUpperCase();
}
if (user.email) {
return user.email.substring(0, 2).toUpperCase();
}
return "?";
};
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>
)}
<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>
)}
</>
);
}
export default function Navigation() {
const [isOpen, setIsOpen] = useState(false);
const { user, logout } = useAuth();
const handleLogout = () => {
logout();
window.location.href = "/";
};
return (
<>
<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 */}
<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,37 +107,38 @@ export default function Navigation() {
</a> </a>
</div> </div>
{/* Mobile Burger Button */} <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
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
{isOpen ? ( <svg
<path className="w-6 h-6"
strokeLinecap="round" fill="none"
strokeLinejoin="round" stroke="currentColor"
strokeWidth={2} viewBox="0 0 24 24"
d="M6 18L18 6M6 6l12 12" >
/> {isOpen ? (
) : ( <path
<path strokeLinecap="round"
strokeLinecap="round" strokeLinejoin="round"
strokeLinejoin="round" strokeWidth={2}
strokeWidth={2} d="M6 18L18 6M6 6l12 12"
d="M4 6h16M4 12h16M4 18h16" />
/> ) : (
)} <path
</svg> strokeLinecap="round"
</button> strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
</button>
<AccountAvatar />
</div>
</nav> </nav>
{/* Mobile Menu */}
{isOpen && ( {isOpen && (
<> <>
<div <div
@@ -88,6 +168,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,86 @@
import {
createContext,
useContext,
useState,
useEffect,
type ReactNode,
} from "react";
interface User {
name?: string;
email?: string;
avatar?: string;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
checkAuth: () => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const checkAuth = async () => {
setIsLoading(true);
try {
const token = localStorage.getItem("auth_token");
if (!token) {
setUser(null);
setIsLoading(false);
return;
}
const response = await fetch("/api/v1/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");
setUser(null);
}
} catch (error) {
console.error("Auth check failed:", error);
localStorage.removeItem("auth_token");
setUser(null);
} finally {
setIsLoading(false);
}
};
const logout = () => {
localStorage.removeItem("auth_token");
setUser(null);
};
useEffect(() => {
checkAuth();
}, []);
return (
<AuthContext.Provider value={{ user, isLoading, checkAuth, logout }}>
{children}
</AuthContext.Provider>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}

View File

@@ -0,0 +1,561 @@
// frontend/src/pages/Admin.tsx
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
interface Post {
id: number;
title: string;
content: string;
published: boolean;
created_at: string;
}
function Admin() {
const [posts, setPosts] = useState<Post[]>([]);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [published, setPublished] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
useEffect(() => {
checkAuth();
fetchPosts();
}, []);
const checkAuth = async () => {
const token = localStorage.getItem("auth_token");
if (!token) {
navigate("/login");
return;
}
try {
const response = await fetch("/api/v1/session", {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
localStorage.removeItem("auth_token");
navigate("/login");
}
} catch (error) {
console.error("Auth check failed:", error);
navigate("/login");
}
};
const fetchPosts = async () => {
const token = localStorage.getItem("auth_token");
try {
const response = await fetch("/api/v1/admin/posts", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setPosts(data.data || []);
} else if (response.status === 401) {
localStorage.removeItem("auth_token");
navigate("/login");
}
} catch (error) {
console.error("Failed to fetch posts:", error);
}
};
const handleCreate = async () => {
if (!title.trim() || !content.trim()) {
alert("Title and content are required");
return;
}
setIsLoading(true);
const token = localStorage.getItem("auth_token");
try {
const response = await fetch("/api/v1/posts/", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
content,
published,
}),
});
if (response.ok) {
setTitle("");
setContent("");
setPublished(false);
setSelectedPost(null);
fetchPosts();
alert("Post created successfully!");
} else {
const error = await response.text();
alert("Failed to create post: " + error);
}
} catch (error) {
console.error("Failed to create post:", error);
alert("Network error");
} finally {
setIsLoading(false);
}
};
const handleUpdate = async () => {
if (!selectedPost || !title.trim() || !content.trim()) {
alert("Title and content are required");
return;
}
setIsLoading(true);
const token = localStorage.getItem("auth_token");
try {
const response = await fetch(`/api/v1/posts/${selectedPost.id}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
content,
published,
}),
});
if (response.ok) {
setTitle("");
setContent("");
setPublished(false);
setSelectedPost(null);
fetchPosts();
alert("Post updated successfully!");
} else {
const error = await response.text();
alert("Failed to update post: " + error);
}
} catch (error) {
console.error("Failed to update post:", error);
alert("Network error");
} finally {
setIsLoading(false);
}
};
const handleDelete = async (id: number) => {
if (!confirm("Delete this post? This action cannot be undone.")) return;
const token = localStorage.getItem("auth_token");
try {
const response = await fetch(`/api/v1/posts/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
fetchPosts();
if (selectedPost?.id === id) {
setSelectedPost(null);
setTitle("");
setContent("");
setPublished(false);
}
alert("Post deleted successfully!");
} else {
alert("Failed to delete post");
}
} catch (error) {
console.error("Failed to delete post:", error);
alert("Network error");
}
};
const selectPost = (post: Post) => {
setSelectedPost(post);
setTitle(post.title);
setContent(post.content);
setPublished(post.published);
setShowPreview(false);
};
const clearForm = () => {
setSelectedPost(null);
setTitle("");
setContent("");
setPublished(false);
setShowPreview(false);
};
// Markdown shortcut helpers
const insertMarkdown = (syntax: string, placeholder: string = "") => {
const textarea = document.querySelector("textarea");
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = content.substring(start, end) || placeholder;
const beforeText = content.substring(0, start);
const afterText = content.substring(end);
let newText = "";
let cursorPosition = start;
if (syntax === "link") {
newText = `${beforeText}[${selectedText}](url)${afterText}`;
cursorPosition = start + selectedText.length + 3;
} else if (syntax === "image") {
newText = `${beforeText}![${selectedText}](image-url)${afterText}`;
cursorPosition = start + selectedText.length + 4;
} else if (syntax === "code") {
newText = `${beforeText}\`\`\`\n${selectedText}\n\`\`\`${afterText}`;
cursorPosition = start + 4;
} else if (syntax === "bold") {
newText = `${beforeText}**${selectedText}**${afterText}`;
cursorPosition = start + 2 + selectedText.length;
} else if (syntax === "italic") {
newText = `${beforeText}*${selectedText}*${afterText}`;
cursorPosition = start + 1 + selectedText.length;
} else if (syntax === "h1") {
newText = `${beforeText}# ${selectedText}${afterText}`;
cursorPosition = start + 2 + selectedText.length;
} else if (syntax === "h2") {
newText = `${beforeText}## ${selectedText}${afterText}`;
cursorPosition = start + 3 + selectedText.length;
} else if (syntax === "list") {
newText = `${beforeText}- ${selectedText}${afterText}`;
cursorPosition = start + 2 + selectedText.length;
}
setContent(newText);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(cursorPosition, cursorPosition);
}, 0);
};
return (
<div className="min-h-screen bg-black text-white font-['Commit_Mono',monospace]">
<div className="max-w-[2000px] mx-auto px-2 sm:px-4">
{/* Header */}
<div className="pt-6 pb-4 border-b border-gray-800">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-[hsl(270,73%,63%)]">
Admin Panel
</h1>
<p className="text-gray-500 text-xs sm:text-sm mt-1">
Blog Editor
</p>
</div>
<div className="flex gap-2 sm:gap-3">
<button
onClick={() => navigate("/admin/upload")}
className="px-3 py-1.5 sm:px-4 sm:py-2 border border-gray-700 rounded hover:border-[hsl(270,73%,63%)] transition-colors text-xs sm:text-sm"
>
Upload
</button>
<button
onClick={() => {
localStorage.removeItem("auth_token");
navigate("/");
}}
className="px-3 py-1.5 sm:px-4 sm:py-2 border border-gray-700 rounded hover:border-red-500 hover:text-red-500 transition-colors text-xs sm:text-sm"
>
Logout
</button>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-3 sm:gap-4 mt-4">
{/* Sidebar - Posts List - Адаптивная ширина */}
<div className="col-span-12 xl:col-span-2">
<div className="xl:sticky xl:top-6">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base sm:text-lg font-semibold">
Posts ({posts.length})
</h2>
<button
onClick={clearForm}
className="px-2 py-1 sm:px-3 sm:py-1 bg-[hsl(270,73%,63%)] text-white rounded text-xs sm:text-sm hover:bg-[hsl(270,73%,70%)] transition-colors"
>
+ New
</button>
</div>
<div className="space-y-2 max-h-[300px] xl:max-h-[calc(100vh-200px)] overflow-y-auto custom-scrollbar">
{posts.map((post) => (
<div
key={post.id}
onClick={() => selectPost(post)}
className={`p-2 sm:p-3 rounded border cursor-pointer transition-all ${
selectedPost?.id === post.id
? "border-[hsl(270,73%,63%)] bg-[hsl(270,73%,63%)]/10"
: "border-gray-800 hover:border-gray-700"
}`}
>
<div className="flex items-start justify-between mb-1">
<h3 className="font-medium text-xs sm:text-sm truncate flex-1">
{post.title}
</h3>
<div className="flex items-center gap-1 sm:gap-2">
{post.published ? (
<span
className="text-xs text-green-500"
title="Published"
>
</span>
) : (
<span className="text-xs text-gray-600" title="Draft">
</span>
)}
<span className="text-xs text-gray-500">
#{post.id}
</span>
</div>
</div>
<p className="text-xs text-gray-500 line-clamp-2 hidden sm:block">
{post.content.substring(0, 60)}...
</p>
<div className="flex items-center justify-between mt-1 sm:mt-2">
<span className="text-xs text-gray-600">
{new Date(post.created_at).toLocaleDateString()}
</span>
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(post.id);
}}
className="text-xs text-red-500 hover:text-red-400"
>
Del
</button>
</div>
</div>
))}
</div>
</div>
</div>
{/* Main Editor - Увеличенная рабочая область */}
<div className="col-span-12 xl:col-span-10">
<div className="bg-black border border-gray-800 rounded-lg">
{/* Toolbar */}
<div className="border-b border-gray-800 p-2 sm:p-3 flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-1 sm:gap-2 flex-wrap">
<button
onClick={() => insertMarkdown("bold", "bold text")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="Bold"
>
<strong>B</strong>
</button>
<button
onClick={() => insertMarkdown("italic", "italic text")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="Italic"
>
<em>I</em>
</button>
<div className="w-px h-4 sm:h-6 bg-gray-800"></div>
<button
onClick={() => insertMarkdown("h1", "Heading 1")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="H1"
>
H1
</button>
<button
onClick={() => insertMarkdown("h2", "Heading 2")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="H2"
>
H2
</button>
<div className="w-px h-4 sm:h-6 bg-gray-800"></div>
<button
onClick={() => insertMarkdown("link", "link text")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="Link"
>
🔗
</button>
<button
onClick={() => insertMarkdown("image", "alt text")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="Image"
>
🖼
</button>
<button
onClick={() => insertMarkdown("code", "code")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="Code"
>
&lt;/&gt;
</button>
<button
onClick={() => insertMarkdown("list", "list item")}
className="p-1.5 sm:p-2 hover:bg-gray-900 rounded transition-colors text-xs sm:text-sm"
title="List"
>
</button>
</div>
<button
onClick={() => setShowPreview(!showPreview)}
className={`px-3 py-1.5 sm:px-4 sm:py-2 rounded text-xs sm:text-sm transition-colors ${
showPreview
? "bg-[hsl(270,73%,63%)] text-white"
: "border border-gray-700 hover:border-gray-600"
}`}
>
{showPreview ? "📝 Editor" : "👁️ Preview"}
</button>
</div>
{/* Content Area */}
<div className="p-3 sm:p-4 md:p-6">
{!showPreview ? (
<>
<input
type="text"
placeholder="Post title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full bg-transparent border-none outline-none text-2xl sm:text-3xl font-bold mb-4 sm:mb-6 placeholder-gray-700"
/>
{/* Чекбокс Published */}
<div className="mb-4 sm:mb-6 flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={published}
onChange={(e) => setPublished(e.target.checked)}
className="w-4 h-4 sm:w-5 sm:h-5 rounded border-gray-700 bg-transparent text-[hsl(270,73%,63%)] focus:ring-[hsl(270,73%,63%)] focus:ring-offset-0 cursor-pointer"
/>
<span className="text-xs sm:text-sm">
Publish this post
</span>
</label>
<span className="text-xs text-gray-600">
{published
? "(Visible to everyone)"
: "(Draft - only admins)"}
</span>
</div>
<textarea
placeholder="Write your post content in Markdown...
Examples:
# Heading
**bold** *italic*
[link](url)
![image](url)
\`\`\`code\`\`\`
"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full h-[400px] sm:h-[500px] md:h-[600px] lg:h-[700px] bg-transparent border border-gray-800 rounded p-3 sm:p-4 outline-none focus:border-gray-700 transition-colors resize-none font-mono text-xs sm:text-sm"
/>
</>
) : (
<div className="prose prose-invert prose-sm sm:prose-lg max-w-none">
<h1 className="text-3xl sm:text-4xl font-bold mb-4 sm:mb-6 text-[hsl(270,73%,63%)]">
{title || "Untitled Post"}
</h1>
<div className="mb-4 sm:mb-6 text-xs sm:text-sm">
{published ? (
<span className="text-green-500"> Published</span>
) : (
<span className="text-yellow-500"> Draft</span>
)}
</div>
<div className="markdown-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{content || "*No content yet...*"}
</ReactMarkdown>
</div>
</div>
)}
</div>
{/* Actions */}
<div className="border-t border-gray-800 p-3 sm:p-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div className="text-xs sm:text-sm text-gray-500">
{selectedPost
? `Editing post #${selectedPost.id}`
: "Creating new post"}
</div>
<div className="flex gap-2 sm:gap-3 w-full sm:w-auto">
{selectedPost && (
<button
onClick={clearForm}
className="flex-1 sm:flex-none px-3 py-1.5 sm:px-4 sm:py-2 border border-gray-700 rounded hover:border-gray-600 transition-colors text-xs sm:text-sm"
>
Cancel
</button>
)}
<button
onClick={selectedPost ? handleUpdate : handleCreate}
disabled={isLoading || !title.trim() || !content.trim()}
className="flex-1 sm:flex-none px-4 py-1.5 sm:px-6 sm:py-2 bg-[hsl(270,73%,63%)] text-white rounded hover:bg-[hsl(270,73%,70%)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-xs sm:text-sm font-medium"
>
{isLoading
? "Saving..."
: selectedPost
? "Update"
: published
? "Publish"
: "Save Draft"}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<style>{`
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #111;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #444;
}
`}</style>
</div>
);
}
export default Admin;

151
frontend/src/pages/Blog.tsx Normal file
View File

@@ -0,0 +1,151 @@
// frontend/src/pages/Blog.tsx
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
interface Post {
id: number;
title: string;
content: string;
created_at: string;
}
function Blog() {
const [posts, setPosts] = useState<Post[]>([]);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
fetchPosts();
}, []);
const fetchPosts = async () => {
try {
const response = await fetch("/api/v1/posts");
if (response.ok) {
const data = await response.json();
const fetchedPosts = data.data || [];
// Сортируем от новых к старым
const sorted = fetchedPosts.sort((a: Post, b: Post) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return dateB - dateA; // От нового к старому
});
setPosts(sorted);
}
} catch (error) {
console.error("Failed to fetch posts:", error);
} finally {
setIsLoading(false);
}
};
const truncateContent = (content: string, sentences: number = 3) => {
const plainText = content
.replace(/#{1,6}\\s/g, "")
.replace(/\\*\\*(.+?)\\*\\*/g, "$1")
.replace(/\\*(.+?)\\*/g, "$1")
.replace(/`{3}[\\s\\S]*?`{3}/g, "")
.replace(/`(.+?)`/g, "$1")
.replace(/\\[(.+?)\\]\\(.+?\\)/g, "$1")
.replace(/!\\[.*?\\]\\(.+?\\)/g, "")
.replace(/\\n+/g, " ")
.trim();
const sentenceRegex = /[^.!?]+[.!?]+/g;
const matches = plainText.match(sentenceRegex);
if (!matches || matches.length === 0) {
if (plainText.length <= 200) return plainText;
return plainText.substring(0, 200).trim() + "...";
}
const truncated = matches.slice(0, sentences).join(" ");
return truncated + (matches.length > sentences ? "..." : "");
};
const formatDate = (dateString: string) => {
if (!dateString) return "";
const date = new Date(dateString);
if (isNaN(date.getTime())) {
console.error("Invalid date from API:", dateString);
return "recent";
}
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
if (isLoading) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-white text-lg font-['Commit_Mono',monospace]">
Loading...
</div>
</div>
);
}
return (
<div className="min-h-screen bg-black text-white font-['Commit_Mono',monospace]">
<div className="max-w-[900px] mx-auto px-4 sm:px-6 py-10 sm:py-16">
{/* Минимальный заголовок */}
<div className="mb-10">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-400 mb-1">
blog
</h1>
<div className="h-px w-24 bg-gray-800" />
</div>
{posts.length === 0 ? (
<div className="py-16">
<p className="text-gray-500 text-sm">No posts yet.</p>
</div>
) : (
<div className="space-y-10 sm:space-y-12">
{posts.map((post, index) => (
<div key={post.id}>
<article className="flex flex-col gap-3 text-left">
<h2 className="text-xl sm:text-2xl font-semibold text-[hsl(270,73%,63%)]">
{post.title}
</h2>
<div className="text-xs sm:text-sm text-gray-500">
{formatDate(post.created_at)}
</div>
<p className="text-gray-300 text-sm sm:text-base leading-relaxed">
{truncateContent(post.content, 3)}
</p>
<div className="mt-2">
<button
onClick={() => navigate(`/blog/${post.id}`)}
className="inline-flex items-center px-4 py-2 border border-gray-700 rounded hover:border-[hsl(270,73%,63%)] hover:text-[hsl(270,73%,63%)] transition-colors text-xs sm:text-sm"
>
Read more
<span className="ml-2 text-[hsl(270,73%,63%)]"></span>
</button>
</div>
</article>
{index !== posts.length - 1 && (
<div className="mt-6 sm:mt-8">
<div className="h-px w-full bg-gradient-to-r from-[hsl(270,73%,63%)] via-gray-800 to-transparent opacity-60" />
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}
export default Blog;

View File

@@ -0,0 +1,284 @@
// frontend/src/pages/BlogPost.tsx
import { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark as theme } from "react-syntax-highlighter/dist/esm/styles/prism";
interface Post {
id: number;
title: string;
content: string;
created_at: string;
}
function BlogPost() {
const { id } = useParams<{ id: string }>();
const [post, setPost] = useState<Post | null>(null);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
if (id) {
fetchPost(id);
}
}, [id]);
const fetchPost = async (postId: string) => {
try {
const response = await fetch(`/api/v1/posts/${postId}`);
if (response.ok) {
const data = await response.json();
setPost(data.data || data);
} else {
navigate("/blog");
}
} catch (error) {
console.error("Failed to fetch post:", error);
navigate("/blog");
} finally {
setIsLoading(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const copyCode = useCallback((code: string) => {
navigator.clipboard.writeText(code);
}, []);
if (isLoading) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-[hsl(270,73%,63%)] text-xl font-['Commit_Mono',monospace]">
Loading...
</div>
</div>
);
}
if (!post) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-gray-500 text-xl font-['Commit_Mono',monospace]">
Post not found
</div>
</div>
);
}
return (
<div className="min-h-screen bg-black text-white font-['Commit_Mono',monospace]">
<div className="max-w-[900px] mx-auto px-4 sm:px-6 py-12 sm:py-16">
<button
onClick={() => navigate("/blog")}
className="flex items-center gap-2 text-gray-400 hover:text-[hsl(270,73%,63%)] transition-colors mb-8 text-sm sm:text-base"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to blog page
</button>
<article>
<header className="mb-8 sm:mb-12">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[hsl(270,73%,63%)] mb-4 leading-tight">
{post.title}
</h1>
<div className="flex items-center gap-4 text-sm text-gray-500">
<time dateTime={post.created_at}>
{formatDate(post.created_at)}
</time>
<span></span>
<span>{Math.ceil(post.content.length / 1000)} min reading</span>
</div>
</header>
<div className="blog-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code(props) {
const { children, className, ...rest } = props;
const match = /language-(\w+)/.exec(className || "");
const language = match ? match[1] : "text";
if (!match) {
return (
<code {...rest} className="inline-code">
{children}
</code>
);
}
return (
<div className="code-container">
<button
onClick={() =>
copyCode(String(children).replace(/\n$/, ""))
}
className="copy-button"
title="Copy code"
>
<span>Copy</span>
</button>
<SyntaxHighlighter
style={theme}
language={language}
PreTag="div"
customStyle={{
background: "rgb(12, 12, 22)",
margin: 0,
padding: "1.75rem 1.5rem 1.5rem",
borderRadius: "0 0 12px 12px",
fontSize: "0.875rem",
lineHeight: "1.65",
}}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</div>
);
},
strong: ({ children }) => (
<strong className="text-white font-bold">{children}</strong>
),
em: ({ children }) => (
<em className="text-white italic">{children}</em>
),
}}
>
{post.content}
</ReactMarkdown>
</div>
</article>
</div>
<style>{`
.blog-content {
color: #ffffff !important;
line-height: 1.8;
}
.blog-content p {
color: #ffffff !important;
margin-bottom: 1.5em;
}
.blog-content strong {
color: #ffffff !important;
font-weight: 700 !important;
}
.blog-content ul, .blog-content ol {
color: #ffffff !important;
padding-left: 1.5em;
}
.blog-content li {
color: #ffffff !important;
}
/* Inline код */
.inline-code {
color: #ffffff !important;
background: rgba(255,255,255,0.1) !important;
padding: 0.2em 0.4em !important;
border-radius: 6px !important;
font-size: 0.875em !important;
font-family: 'Commit_Mono', monospace !important;
}
/* Контейнер кода */
.code-container {
position: relative;
margin: 2em 0;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(145deg, rgb(8,8,18) 0%, rgb(18,18,32) 100%);
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
/* Анимированная кнопка */
.copy-button {
position: absolute;
top: 14px;
right: 14px;
z-index: 20;
background: linear-gradient(135deg, rgba(117,43,255,0.25) 0%, rgba(117,43,255,0.15) 100%);
border: 1px solid rgba(117,43,255,0.5);
color: #ffffff;
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: 'Commit_Mono', monospace;
backdrop-filter: blur(20px);
box-shadow: 0 4px 16px rgba(117,43,255,0.25);
}
.copy-button:hover {
background: linear-gradient(135deg, hsl(270,73%,65%) 0%, hsl(270,73%,55%) 100%);
transform: translateY(-3px) scale(1.03);
box-shadow: 0 12px 30px rgba(117,43,255,0.45);
border-color: hsl(270,73%,75%);
}
.copy-button:active {
transform: translateY(-1px) scale(0.98);
transition: all 0.1s;
}
/* Заголовки */
.blog-content h1, .blog-content h2, .blog-content h3 {
color: hsl(270, 73%, 63%) !important;
margin-top: 2em;
margin-bottom: 0.75em;
}
.blog-content a {
color: hsl(270, 73%, 63%) !important;
}
/* Responsive */
@media (max-width: 640px) {
.code-container {
margin: 1.5em -1rem;
border-radius: 0;
border-left: none;
border-right: none;
}
.copy-button {
top: 10px;
right: 10px;
padding: 6px 12px;
font-size: 12px;
}
}
`}</style>
</div>
);
}
export default BlogPost;

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

@@ -0,0 +1,323 @@
// frontend/src/pages/Upload.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
interface UploadedFile {
url: string;
filename: string;
size: number;
uploaded_at: string;
}
function Upload() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [dragActive, setDragActive] = useState(false);
const navigate = useNavigate();
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setSelectedFile(e.target.files[0]);
}
};
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
setSelectedFile(e.dataTransfer.files[0]);
}
};
const handleUpload = async () => {
if (!selectedFile) {
alert("Please select a file first");
return;
}
setIsUploading(true);
const token = localStorage.getItem("auth_token");
const formData = new FormData();
formData.append("file", selectedFile);
try {
const response = await fetch("/api/v1/upload", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (response.ok) {
const data = await response.json();
const newFile: UploadedFile = {
url: data.data?.url || data.url,
filename: selectedFile.name,
size: selectedFile.size,
uploaded_at: new Date().toISOString(),
};
setUploadedFiles([newFile, ...uploadedFiles]);
setSelectedFile(null);
alert("File uploaded successfully!");
} else {
const error = await response.text();
alert("Upload failed: " + error);
}
} catch (error) {
console.error("Upload error:", error);
alert("Network error");
} finally {
setIsUploading(false);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
alert("Copied to clipboard!");
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
return (bytes / (1024 * 1024)).toFixed(2) + " MB";
};
return (
<div className="min-h-screen bg-black text-white font-['Commit_Mono',monospace]">
<div className="max-w-[1400px] mx-auto px-2 sm:px-4">
{/* Header */}
<div className="pt-6 pb-4 border-b border-gray-800">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-[hsl(270,73%,63%)]">
File Upload
</h1>
<p className="text-gray-500 text-xs sm:text-sm mt-1">
Upload images and files
</p>
</div>
<div className="flex gap-2 sm:gap-3">
<button
onClick={() => navigate("/admin")}
className="px-3 py-1.5 sm:px-4 sm:py-2 border border-gray-700 rounded hover:border-[hsl(270,73%,63%)] transition-colors text-xs sm:text-sm"
>
Editor
</button>
<button
onClick={() => {
localStorage.removeItem("auth_token");
navigate("/");
}}
className="px-3 py-1.5 sm:px-4 sm:py-2 border border-gray-700 rounded hover:border-red-500 hover:text-red-500 transition-colors text-xs sm:text-sm"
>
Logout
</button>
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Upload Section */}
<div>
<h2 className="text-lg font-semibold mb-4">Upload New File</h2>
<div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-all ${
dragActive
? "border-[hsl(270,73%,63%)] bg-[hsl(270,73%,63%)]/10"
: "border-gray-700 hover:border-gray-600"
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<div className="mb-4">
<svg
className="mx-auto h-12 w-12 text-gray-600"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div className="mb-4">
{selectedFile ? (
<div className="text-sm">
<p className="font-medium text-[hsl(270,73%,63%)]">
{selectedFile.name}
</p>
<p className="text-gray-500 mt-1">
{formatFileSize(selectedFile.size)}
</p>
</div>
) : (
<>
<p className="text-sm text-gray-400 mb-2">
Drag and drop a file here, or click to select
</p>
<p className="text-xs text-gray-600">
Supported: Images, Documents, Archives
</p>
</>
)}
</div>
<input
type="file"
onChange={handleFileSelect}
className="hidden"
id="file-input"
/>
<label
htmlFor="file-input"
className="inline-block px-4 py-2 border border-gray-700 rounded hover:border-gray-600 transition-colors cursor-pointer text-sm"
>
Select File
</label>
</div>
{selectedFile && (
<div className="mt-4 flex gap-3">
<button
onClick={handleUpload}
disabled={isUploading}
className="flex-1 px-6 py-3 bg-[hsl(270,73%,63%)] text-white rounded hover:bg-[hsl(270,73%,70%)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{isUploading ? "Uploading..." : "Upload File"}
</button>
<button
onClick={() => setSelectedFile(null)}
className="px-4 py-3 border border-gray-700 rounded hover:border-gray-600 transition-colors"
>
Clear
</button>
</div>
)}
</div>
{/* Uploaded Files Section */}
<div>
<h2 className="text-lg font-semibold mb-4">
Uploaded Files ({uploadedFiles.length})
</h2>
<div className="space-y-3 max-h-[600px] overflow-y-auto custom-scrollbar">
{uploadedFiles.length === 0 ? (
<div className="text-center py-12 text-gray-600">
<p>No files uploaded yet</p>
<p className="text-sm mt-2">Upload files to see them here</p>
</div>
) : (
uploadedFiles.map((file, index) => (
<div
key={index}
className="border border-gray-800 rounded-lg p-4 hover:border-gray-700 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{file.filename}
</p>
<p className="text-xs text-gray-500 mt-1">
{formatFileSize(file.size)} {" "}
{new Date(file.uploaded_at).toLocaleDateString()}
</p>
</div>
</div>
{/* Preview if image */}
{file.filename.match(/\.(jpg|jpeg|png|gif|webp)$/i) && (
<img
src={file.url}
alt={file.filename}
className="w-full h-32 object-cover rounded mt-3 mb-3 border border-gray-800"
/>
)}
<div className="space-y-2">
{/* URL */}
<div className="flex items-center gap-2">
<input
type="text"
value={file.url}
readOnly
className="flex-1 bg-gray-900 border border-gray-800 rounded px-3 py-2 text-xs font-mono"
/>
<button
onClick={() => copyToClipboard(file.url)}
className="px-3 py-2 border border-gray-700 rounded hover:border-gray-600 transition-colors text-xs"
>
Copy
</button>
</div>
{/* Markdown */}
{file.filename.match(/\.(jpg|jpeg|png|gif|webp)$/i) && (
<div className="flex items-center gap-2">
<input
type="text"
value={`![${file.filename}](${file.url})`}
readOnly
className="flex-1 bg-gray-900 border border-gray-800 rounded px-3 py-2 text-xs font-mono"
/>
<button
onClick={() =>
copyToClipboard(
`![${file.filename}](${file.url})`,
)
}
className="px-3 py-2 border border-gray-700 rounded hover:border-gray-600 transition-colors text-xs"
>
MD
</button>
</div>
)}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
<style>{`
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #111;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #444;
}
`}</style>
</div>
);
}
export default Upload;

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,
},
},
},
}); });