From b60de5f97c2c2f85780bb1a0fed7d9b02aafe156 Mon Sep 17 00:00:00 2001 From: Yigid BALABAN Date: Wed, 15 Oct 2025 14:11:23 +0300 Subject: [PATCH] going prod step-by-step --- example.caddy | 21 ++ internal/http/handlers.go | 8 + internal/http/router.go | 1 + openapi.yaml | 444 ++++++++++++++++++++++++++++++++++++++ pre-docker-deploy.sh | 5 +- 5 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 example.caddy create mode 100644 openapi.yaml diff --git a/example.caddy b/example.caddy new file mode 100644 index 0000000..e6b2137 --- /dev/null +++ b/example.caddy @@ -0,0 +1,21 @@ +docs.example.org { + # API requests go to Docker container + handle /api* { + reverse_proxy HOST:PORT # TODO: set to HOST and PORT from .env + } + + handle { + file_server { + root $COMPOSE_DIR/volumes/docs # TODO: change $COMPOSE_DIR to the actual path + index index.html + browse off + } + } + + #log { + # output_file /var/log/caddy/docs.example.org.log { + # roll_size 10MB + # roll_keep 10 + # } + #} +} \ No newline at end of file diff --git a/internal/http/handlers.go b/internal/http/handlers.go index ecfcb19..e19178e 100644 --- a/internal/http/handlers.go +++ b/internal/http/handlers.go @@ -202,6 +202,14 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { }) } +func (s *Server) handleHello(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{ + "message": "Close the world, .txen eht nepO", + "author": "Yigid BALABAN ", + "authorHomepage": "https://yigid.dev/", + }) +} + func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/internal/http/router.go b/internal/http/router.go index 9586daf..10ee61f 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -23,6 +23,7 @@ func NewRouter(cfg *config.Config, userMgr *user.Manager, deployMgr *deploy.Mana mux.Handle("DELETE /api/v1/auth", adminAuth(http.HandlerFunc(server.handleAuthDelete))) mux.Handle("POST /api/v1/deploy", userAuth(maxBytes(http.HandlerFunc(server.handleDeploy)))) mux.HandleFunc("GET /api/v1/status", server.handleStatus) + mux.HandleFunc("GET /api/hello", server.handleHello) return LoggingMiddleware(logger)(mux) } diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..bcf56b2 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,444 @@ +openapi: 3.0.3 +info: + title: Tingz Deployer API + description: A secure, lightweight HTTP service for deploying static sites. The service receives tarball artifacts, validates and publishes them under a per-user directory structure for docs.yigid.dev. Designed for CI/CD automation with GitHub Actions, GitLab CI, etc. + version: 1.0.0 + contact: + name: Yigid BALABAN + email: hey@yigid.dev + url: https://yigid.dev/ + license: + name: AGPL-3.0-only + url: https://opensource.org/license/agpl-v3 +servers: + - url: https://docs.yigid.dev + description: Production server + - url: http://localhost:8080 + description: Development server +paths: + /api/v1/auth: + post: + tags: + - Authentication + summary: Create or reroll user token + description: Creates a new user or generates a new token for an existing user. Admin only endpoint. + operationId: createOrRerollUser + security: + - AdminAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequest' + example: + username: alice + responses: + '200': + description: User created or token rerolled successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthResponse' + example: + status: ok + token: 82lz0BVOWtV4uVj9DBZBuKMeyVDwidL2 + '400': + description: Invalid request body or username format + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidBody: + summary: Invalid request body + value: + status: error + error: invalid request body + invalidUsername: + summary: Invalid username format + value: + status: error + error: invalid username format + '401': + description: Unauthorized - Invalid admin token + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: error + error: invalid admin token + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: error + error: failed to create user + delete: + tags: + - Authentication + summary: Delete user + description: Deletes a user and optionally their files. Admin only endpoint. + operationId: deleteUser + security: + - AdminAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteUserRequest' + examples: + persistFiles: + summary: Delete user but keep files + value: + username: alice + files: persist + deleteFiles: + summary: Delete user and all files + value: + username: alice + files: delete + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + example: + status: ok + '400': + description: Invalid request body, username format, or files parameter + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidBody: + summary: Invalid request body + value: + status: error + error: invalid request body + invalidUsername: + summary: Invalid username format + value: + status: error + error: invalid username format + invalidFiles: + summary: Invalid files parameter + value: + status: error + error: files must be 'persist' or 'delete' + '401': + description: Unauthorized - Invalid admin token + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: error + error: invalid admin token + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: error + error: user not found + /api/v1/deploy: + post: + tags: + - Deployment + summary: Deploy static site + description: Uploads and deploys a static site tarball. Creates a new release and atomically updates the live site via symlink swap. + operationId: deploySite + security: + - UserAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - project + - file + properties: + project: + type: string + description: Project name (must match username validation pattern) + pattern: ^[a-z0-9]([a-z0-9_-]{0,62}[a-z0-9])?$ + minLength: 1 + maxLength: 64 + example: myproject + file: + type: string + format: binary + description: Tarball file (.tar.gz) containing the static site + responses: + '200': + description: Deployment successful + content: + application/json: + schema: + $ref: '#/components/schemas/DeployResponse' + example: + status: ok + project: myproject + username: alice + url: https://docs.yigid.dev/alice/myproject/ + release: 20251014T180806 + '400': + description: Invalid request - missing file, invalid project name, or multipart form parsing error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + missingFile: + summary: Missing or invalid file + value: + status: error + error: missing or invalid file + invalidProject: + summary: Invalid project name format + value: + status: error + error: invalid project name format + parseError: + summary: Failed to parse multipart form + value: + status: error + error: failed to parse multipart form + '401': + description: Unauthorized - Invalid user token + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: error + error: unauthorized + '413': + description: Request entity too large - Upload exceeds maximum size limit + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + status: error + error: request body too large + '500': + description: Internal server error during deployment + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + extractionError: + summary: Failed to extract tarball + value: + status: error + error: 'failed to extract tarball: path contains ''..'' component which is not allowed: ../../etc/passwd' + generalError: + summary: General deployment error + value: + status: error + error: deployment failed + /api/v1/status: + get: + tags: + - Health + summary: Health check + description: Returns the current status of the service + operationId: getStatus + responses: + '200': + description: Service is healthy + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + example: + status: ok + /api/hello: + get: + tags: + - Info + summary: Service information + description: Returns basic information about the service and author + operationId: getHello + responses: + '200': + description: Service information + content: + application/json: + schema: + $ref: '#/components/schemas/HelloResponse' + example: + message: Close the world, .txen eht nepO + author: Yigid BALABAN + authorHomepage: https://yigid.dev/ +components: + securitySchemes: + AdminAuth: + type: http + scheme: bearer + description: Admin bearer token for user management operations + UserAuth: + type: http + scheme: bearer + description: User bearer token for deployment operations + schemas: + CreateUserRequest: + type: object + required: + - username + properties: + username: + type: string + description: Username for the new user (lowercase letters, numbers, hyphens, underscores only) + pattern: ^[a-z0-9]([a-z0-9_-]{0,62}[a-z0-9])?$ + minLength: 1 + maxLength: 64 + example: alice + DeleteUserRequest: + type: object + required: + - username + properties: + username: + type: string + description: Username of the user to delete + pattern: ^[a-z0-9]([a-z0-9_-]{0,62}[a-z0-9])?$ + minLength: 1 + maxLength: 64 + example: alice + files: + type: string + description: Whether to persist or delete user files + enum: + - persist + - delete + default: persist + example: persist + AuthResponse: + type: object + required: + - status + - token + properties: + status: + type: string + enum: + - ok + example: ok + token: + type: string + description: 32-character alphanumeric token for API access + minLength: 32 + maxLength: 32 + example: 82lz0BVOWtV4uVj9DBZBuKMeyVDwidL2 + DeployResponse: + type: object + required: + - status + - project + - username + - url + - release + properties: + status: + type: string + enum: + - ok + example: ok + project: + type: string + description: Project name + example: myproject + username: + type: string + description: Username who deployed + example: alice + url: + type: string + format: uri + description: Public URL where the site is accessible + example: https://docs.yigid.dev/alice/myproject/ + release: + type: string + description: 'Release ID (timestamp format: YYYYMMDDTHHMMSS)' + pattern: ^[0-9]{8}T[0-9]{6}$ + example: 20251014T180806 + StatusResponse: + type: object + required: + - status + properties: + status: + type: string + enum: + - ok + example: ok + HelloResponse: + type: object + required: + - message + - author + - authorHomepage + properties: + message: + type: string + description: Service message + example: Close the world, .txen eht nepO + author: + type: string + description: Author information + example: Yigid BALABAN + authorHomepage: + type: string + format: uri + description: Author homepage URL + example: https://yigid.dev/ + SuccessResponse: + type: object + required: + - status + properties: + status: + type: string + enum: + - ok + example: ok + ErrorResponse: + type: object + required: + - status + - error + properties: + status: + type: string + enum: + - error + example: error + error: + type: string + description: Error message describing what went wrong + example: invalid username format +tags: + - name: Authentication + description: User management operations (admin only) + - name: Deployment + description: Static site deployment operations + - name: Health + description: Service health and status endpoints + - name: Info + description: Service information endpoints diff --git a/pre-docker-deploy.sh b/pre-docker-deploy.sh index c78a2f2..7ba9258 100644 --- a/pre-docker-deploy.sh +++ b/pre-docker-deploy.sh @@ -34,9 +34,10 @@ echo "4. Create and set permissions for volumes" mkdir -p volumes/data volumes/docs volumes/deploys chown -R tingz:tingz volumes -echo "5. Create .env.development file" +echo "5. Create .env.tmp file" +ADMIN_TOKEN=$(openssl rand -hex 16) cat > .env.tmp << EOF -#ADMIN_TOKEN= +ADMIN_TOKEN=${ADMIN_TOKEN} HOST=127.0.0.1 PORT=8080 UID=${GID}