├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── authenticator.proto ├── cmd ├── README.md ├── app │ ├── client │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ └── server │ │ ├── audit.log │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go ├── go.mod ├── go.sum └── protoc-gen-rpc-impl.go ├── coverage.txt ├── entrypoint.sh ├── generator.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── google └── api │ ├── annotations.proto │ ├── field_behavior.proto │ ├── http.proto │ ├── httpbody.proto │ └── resource.proto ├── index.html ├── install.sh ├── k8s ├── app-deployment.yaml ├── app-loadbalancer.yaml ├── app-service.yaml ├── hpa.yaml ├── network-policy.yaml ├── pgbouncer-all.yaml ├── postgres-deployment.yaml ├── postgres-pvc.yaml └── postgres-service.yaml ├── pkg ├── db │ ├── .gitignore │ ├── db_gen.go │ ├── db_test.go │ ├── go.mod │ ├── go.sum │ └── query-engine-debian-openssl-3.0.x_gen.go ├── middlewares │ ├── auth.go │ ├── chain_middleware.go │ ├── cors.go │ ├── go.mod │ ├── go.sum │ ├── logging.go │ ├── middlewares_test.go │ └── rate_limiter.go ├── routes │ ├── generated_register.go │ └── go.mod ├── schema.prisma └── services │ ├── auth_integration_test.go │ ├── authenticator.swagger.json │ ├── authenticator_server.go │ ├── generated │ ├── authenticator.pb.go │ ├── authenticator.pb.gw.go │ ├── authenticator_grpc.pb.go │ ├── authenticator_mock.go │ ├── authenticator_server_test.go │ ├── go.mod │ └── go.sum │ ├── go.mod │ ├── go.sum │ ├── jwt.go │ └── jwt_test.go └── services.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Thunder CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: [ main, dev ] 6 | pull_request: 7 | branches: [ main, dev ] 8 | 9 | jobs: 10 | test: 11 | name: Run Tests and Generate Coverage 12 | runs-on: ubuntu-latest 13 | env: 14 | JWT_SECRET: "testsecret" 15 | 16 | services: 17 | postgres: 18 | image: postgres:15 19 | env: 20 | POSTGRES_DB: thunder 21 | POSTGRES_USER: postgres 22 | POSTGRES_PASSWORD: postgres 23 | ports: 24 | - 5432:5432 25 | options: >- 26 | --health-cmd "pg_isready -U testuser -d testdb" 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v4 37 | with: 38 | go-version: '1.21' 39 | 40 | - name: Install Dependencies 41 | run: go mod tidy 42 | 43 | - name: Generate SSL Certificates 44 | run: | 45 | mkdir -p certs 46 | openssl req -x509 -newkey rsa:4096 -keyout certs/server.key -out certs/server.crt -days 365 -nodes \ 47 | -subj "/CN=localhost" \ 48 | -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 49 | 50 | - name: Run Unit Tests 51 | run: go test -v ./... 52 | 53 | - name: Run Integration Tests (gRPC + REST) 54 | run: go test -v ./pkg/db ./pkg/middlewares/ ./pkg/services/ ./pkg/services/generated 55 | 56 | - name: Generate Coverage Report 57 | run: go test -coverprofile=coverage.txt ./pkg/db ./pkg/middlewares/ ./pkg/services/ ./pkg/services/generated 58 | 59 | - name: Print Coverage Summary 60 | run: go tool cover -func=coverage.txt 61 | 62 | - name: Upload Coverage Report 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: coverage-report 66 | path: coverage.txt 67 | 68 | - name: Upload to Codecov 69 | uses: codecov/codecov-action@v4 70 | with: 71 | token: ${{ secrets.CODECOV_TOKEN }} # Optional for public repositories 72 | file: coverage.txt 73 | fail_ci_if_error: true 74 | verbose: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore TLS certificate files and keys 2 | cmd/certs/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | kmosc@protonmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Thunder 2 | 3 | Thank you for your interest in contributing to Thunder! 🎉 We welcome contributions of all kinds—from bug reports and documentation improvements to feature development and optimizations. 4 | 5 | ## How to Contribute 6 | 7 | ### 1️⃣ Fork and Clone the Repository 8 | 9 | 1. Click the **Fork** button on GitHub to create your own copy of the repository. 10 | 2. Clone your forked repository to your local machine: 11 | ```sh 12 | git clone https://github.com/YOUR_USERNAME/Thunder.git 13 | cd Thunder 14 | ``` 15 | 3. Add the upstream repository: 16 | ```sh 17 | git remote add upstream https://github.com/Raezil/Thunder.git 18 | ``` 19 | 20 | ### 2️⃣ Create a Feature Branch 21 | 22 | Before making changes, create a new branch: 23 | ```sh 24 | git checkout -b feature/your-feature-name 25 | ``` 26 | 27 | ### 3️⃣ Make Your Changes 28 | 29 | - Follow Go best practices and project coding standards. 30 | - Write tests for new features or bug fixes. 31 | - Run tests before submitting: 32 | ```sh 33 | go test ./... 34 | ``` 35 | 36 | ### 4️⃣ Commit and Push Changes 37 | 38 | 1. Stage your changes: 39 | ```sh 40 | git add . 41 | ``` 42 | 2. Write a clear commit message: 43 | ```sh 44 | git commit -m "Add feature: description" 45 | ``` 46 | 3. Push your changes: 47 | ```sh 48 | git push origin feature/your-feature-name 49 | ``` 50 | 51 | ### 5️⃣ Submit a Pull Request (PR) 52 | 53 | 1. Open a pull request from your feature branch to `main`. 54 | 2. Provide a clear description of your changes. 55 | 3. Address any feedback from maintainers. 56 | 57 | ## Reporting Issues 58 | 59 | If you encounter a bug or have a feature request: 60 | - Check if the issue already exists in [GitHub Issues](https://github.com/Raezil/Thunder/issues). 61 | - If not, create a new issue with a descriptive title and detailed steps to reproduce (if applicable). 62 | 63 | ## Community Engagement 64 | 65 | Join the discussion and contribute in various ways: 66 | - **GitHub Discussions:** Ask questions, propose features, and get support. 67 | - **Discord:** Join our community for real-time collaboration. 68 | 69 | ## Code of Conduct 70 | 71 | By participating in this project, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md). 72 | 73 | We appreciate your contributions and look forward to collaborating with you! 🚀 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Go image 2 | FROM golang:1.23-alpine 3 | 4 | # Install git (needed for 'go get' in some cases) 5 | RUN apk add --no-cache git 6 | 7 | # Create an app directory 8 | WORKDIR /app 9 | 10 | # Copy module files first 11 | COPY go.mod go.sum ./ 12 | RUN go mod download 13 | 14 | # Copy the rest of your code 15 | COPY . . 16 | 17 | # (Optional) Remove the unnecessary file if it exists 18 | RUN rm -f pkg/db/query-engine-debian-openssl-3.0.x_gen.go 19 | 20 | # Install prisma-client-go 21 | RUN go install github.com/steebchen/prisma-client-go@latest 22 | 23 | # Add Go binaries to PATH 24 | ENV PATH=$PATH:/go/bin 25 | 26 | # Expose necessary ports 27 | EXPOSE 50051 8080 28 | 29 | # Copy the entrypoint script 30 | COPY entrypoint.sh /app/entrypoint.sh 31 | RUN chmod +x /app/entrypoint.sh 32 | 33 | # Copy the certificates directory 34 | COPY cmd/certs /certs 35 | # Set the entrypoint and default command 36 | ENTRYPOINT ["/app/entrypoint.sh"] 37 | CMD ["go", "run", "./cmd/app/server/main.go"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kamil Mosciszko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | centered image 3 |

4 | 5 | # **Thunder- A Minimalist Backend Framework in Go** 6 | 7 | 8 | *A gRPC-Gateway-powered framework with Prisma, Kubernetes, and Go for scalable microservices.* 9 | 10 | [![libs.tech recommends](https://libs.tech/project/882664523/badge.svg)](https://libs.tech/project/882664523/thunder) 11 | [![Go Version](https://img.shields.io/badge/Go-1.23-blue)](https://golang.org) 12 | [![License](https://img.shields.io/github/license/Raezil/Thunder)](LICENSE) 13 | [![Stars](https://img.shields.io/github/stars/Raezil/Thunder)](https://github.com/Raezil/Thunder/stargazers) 14 | 15 | ## **🚀 Features** 16 | - ✔️ **gRPC + REST (gRPC-Gateway)** – Automatically expose RESTful APIs from gRPC services. 17 | - ✔️ **Prisma Integration** – Efficient database management and migrations. 18 | - ✔️ **Kubernetes Ready** – Easily deploy and scale with Kubernetes. 19 | - ✔️ **TLS Security** – Secure gRPC communications with TLS. 20 | - ✔️ **Structured Logging** – Built-in `zap` logging. 21 | - ✔️ **Rate Limiting & Authentication** – Pre-configured middleware. 22 | - ✔️ **Modular & Extensible** – Easily extend Thunder for custom use cases. 23 | - ✔️ **Thunder CLI** - Generate, deploy, and create new projects effortlessly. 24 | 25 | ## **🏗️ Architecture Overview** 26 | ![421386849-54a1cead-6886-400a-a41a-f5eb4f375dc7(1)](https://github.com/user-attachments/assets/5074e533-b023-415d-9092-e8f5270ec88f) 27 | 28 | ## **📌 Use Cases** 29 | 30 | Thunder is designed for **scalable microservices** and **high-performance API development**, particularly suited for: 31 | 32 | ### **1. High-Performance API Development** 33 | - gRPC-first APIs with RESTful interfaces via gRPC-Gateway. 34 | - Critical performance and low latency applications. 35 | - Strongly-typed APIs with protobufs. 36 | 37 | ### **2. Microservices Architecture** 38 | - Efficient inter-service communication. 39 | - Kubernetes deployments with built-in service discovery and scaling. 40 | 41 | ### **3. Database Management with Prisma** 42 | - Type-safe queries and easy database migrations. 43 | - Support for multiple databases (PostgreSQL, MySQL, SQLite). 44 | 45 | ### **4. Lightweight Backend Alternative** 46 | - A minimalist and powerful alternative to traditional frameworks like Gin or Echo. 47 | - Fast, simple, and modular backend without unnecessary overhead. 48 | 49 | ### **5. Kubernetes & Cloud-Native Applications** 50 | - Containerized environments using Docker. 51 | - Automatic service scaling and load balancing. 52 | 53 | ### **When Not to Use Thunder** 54 | - If you need a traditional REST-only API (use Gin, Fiber, or Echo instead). 55 | - If you require a feature-heavy web framework with extensive middleware. 56 | - If you're not deploying on Kubernetes or prefer a monolithic backend. 57 | 58 | ## **📌 Getting Started** 59 | ### **Installation** 60 | ```bash 61 | git clone https://github.com/Raezil/Thunder.git 62 | cd Thunder 63 | chmod +x install.sh 64 | ./install.sh 65 | ``` 66 | > Remember to install prerequisites, there is tutorial for this https://github.com/Raezil/Thunder/issues/99 67 | 68 | ### **Setup** 69 | Create a new Thunder application: 70 | ```bash 71 | thunder init myapp 72 | cd myapp 73 | ``` 74 | 75 | ### **Install Dependencies** 76 | ```bash 77 | go mod tidy 78 | ``` 79 | 80 | ### **Define Your gRPC Service** 81 | Create a `.proto` file (e.g., `example.proto`): 82 | 83 | ```proto 84 | syntax = "proto3"; 85 | 86 | package example; 87 | 88 | import "google/api/annotations.proto"; 89 | 90 | service Example { 91 | rpc SayHello(HelloRequest) returns (HelloResponse) { 92 | option (google.api.http) = { 93 | get: "/v1/example/sayhello" 94 | }; 95 | }; 96 | } 97 | 98 | message HelloRequest { 99 | string name = 1; 100 | } 101 | 102 | message HelloResponse { 103 | string message = 1; 104 | } 105 | ``` 106 | 107 | Add your service entry in `services.json`: 108 | ```go 109 | [ 110 | { 111 | "ServiceName": "Example", 112 | "ServiceStruct": "ExampleServiceServer", 113 | "ServiceRegister": "RegisterExampleServer", 114 | "HandlerRegister": "RegisterExampleHandler" 115 | } 116 | ] 117 | ``` 118 | 119 | ## **🛠️ Prisma Integration** 120 | Define your schema in `schema.prisma`: 121 | 122 | ```prisma 123 | datasource db { 124 | provider = "postgresql" 125 | url = env("DATABASE_URL") 126 | } 127 | 128 | model User { 129 | id String @default(cuid()) @id 130 | name String 131 | email String @unique 132 | } 133 | ``` 134 | 135 | Generate the service implementation: 136 | ```bash 137 | thunder generate --proto=example.proto 138 | ``` 139 | 140 | ## **🚀 Running the Server** 141 | 142 | Start the server: 143 | ```bash 144 | go run ./cmd/app/server/main.go 145 | ``` 146 | 147 | Server accessible via HTTP at `localhost:8080` and gRPC at `localhost:50051`. 148 | 149 | ## **🚀 Running the Tests** 150 | 151 | ### Mocking Tests 152 | ```bash 153 | cd pkg/services/generated 154 | mockgen -source=yourservice_grpc.pb.go -destination=./yourservice_mock.go 155 | ``` 156 | 157 | ### Run Tests 158 | ```bash 159 | go test ./pkg/db ./pkg/middlewares/ ./pkg/services/ ./pkg/services/generated 160 | ``` 161 | 162 | ## **🔧 Kubernetes Deployment** 163 | ### PgBouncer Configuration 164 | 165 | This setup configures PgBouncer to connect to a PostgreSQL database using Kubernetes resources. 166 | 167 | ### Updating the `userlist.txt` Secret 168 | 169 | To regenerate and update the `userlist.txt` secret, use the following command to encode the credentials: 170 | 171 | ```bash 172 | echo '"postgres" "postgres"' | base64 173 | ``` 174 | 175 | Now, update `pgbouncer-all.yaml` under the `Secret` section with the new base64-encoded value: 176 | 177 | ```yaml 178 | apiVersion: v1 179 | kind: Secret 180 | metadata: 181 | name: pgbouncer-secret 182 | type: Opaque 183 | data: 184 | userlist.txt: # "postgres" "postgres" in base64 185 | ``` 186 | 187 | ### Generate TLS Certificates 188 | ```bash 189 | cd cmd 190 | mkdir certs 191 | openssl req -x509 -newkey rsa:4096 -keyout certs/server.key -out certs/server.crt -days 365 -nodes \ 192 | -subj "/CN=localhost" \ 193 | -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 194 | ``` 195 | 196 | ### Generate Kubernetes Secrets 197 | ```bash 198 | kubectl create secret generic app-secret --from-literal=DATABASE_URL="postgres://postgres:postgres@pgbouncer-service:6432/thunder?sslmode=disable" --from-literal=JWT_SECRET="secret" 199 | 200 | kubectl create secret generic postgres-secret --from-literal=POSTGRES_USER=postgres --from-literal=POSTGRES_PASSWORD=postgres --from-literal=POSTGRES_DB=thunder 201 | ``` 202 | 203 | ### Build & Deploy Docker Image 204 | ```bash 205 | thunder build 206 | thunder deploy 207 | ``` 208 | 209 | Check pod status: 210 | ```bash 211 | kubectl get pods -n default 212 | kubectl describe pod $NAME -n default 213 | ``` 214 | 215 | ## **📡 API Testing** 216 | 217 | ### Register User 218 | ```bash 219 | curl -k --http2 -X POST https://localhost:8080/v1/auth/register \ 220 | -H "Content-Type: application/json" \ 221 | -d '{ 222 | "email": "newuser@example.com", 223 | "password": "password123", 224 | "name": "John", 225 | "surname": "Doe", 226 | "age": 30 227 | }' 228 | ``` 229 | 230 | ### User Login 231 | ```bash 232 | curl -k --http2 -X POST https://localhost:8080/v1/auth/login \ 233 | -H "Content-Type: application/json" \ 234 | -d '{ 235 | "email": "newuser@example.com", 236 | "password": "password123" 237 | }' 238 | ``` 239 | 240 | ### Sample protected 241 | ```bash 242 | curl -k -X GET "https://localhost:8080/v1/auth/protected?text=hello" \ 243 | -H "Authorization: $token" 244 | ``` 245 | > $token is returned by login 246 | 247 | 248 | ## **📜 Contributing** 249 | 250 | 1. Fork the repository. 251 | 2. Create a feature branch: `git checkout -b feature-new` 252 | 3. Commit changes: `git commit -m "Added feature"` 253 | 4. Push to your branch: `git push origin feature-new` 254 | 5. Submit a pull request. 255 | 256 | ## **🔗 References** 257 | - [Go Documentation](https://golang.org/doc/) 258 | - [gRPC](https://grpc.io/docs/languages/go/quickstart/) 259 | - [gRPC-Gateway](https://grpc-ecosystem.github.io/grpc-gateway/) 260 | - [Prisma ORM](https://www.prisma.io/docs/) 261 | - [Kubernetes Docs](https://kubernetes.io/docs/) 262 | - [Tutorial](https://gist.github.com/Raezil/f649ae8c5201f60d479ed796299af679) 263 | 264 | ## **📣 Stay Connected** 265 | ⭐ Star the repository if you find it useful! 266 | 📧 For support, use [GitHub Issues](https://github.com/Raezil/Thunder/issues). 267 | 268 | ## **License** 269 | Thunder is released under the MIT License. 270 | -------------------------------------------------------------------------------- /authenticator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package authenticator; 4 | 5 | option go_package = "./pkg/services/generated"; 6 | 7 | import "google/api/annotations.proto"; 8 | 9 | service Auth { 10 | rpc Login (LoginRequest) returns (LoginReply) { 11 | option (google.api.http) = { 12 | post: "/v1/auth/login" 13 | body: "*" 14 | }; 15 | } 16 | 17 | rpc Register (RegisterRequest) returns (RegisterReply) { 18 | option (google.api.http) = { 19 | post: "/v1/auth/register" 20 | body: "*" 21 | }; 22 | } 23 | 24 | rpc SampleProtected (ProtectedRequest) returns (ProtectedReply) { 25 | option (google.api.http) = { 26 | get: "/v1/auth/protected" 27 | }; 28 | } 29 | } 30 | 31 | message ProtectedRequest { 32 | string text = 1; 33 | } 34 | 35 | message ProtectedReply { 36 | string result = 1; 37 | } 38 | 39 | message LoginRequest { 40 | string email = 1; 41 | string password = 2; 42 | } 43 | 44 | message RegisterRequest { 45 | string email = 1; 46 | string password = 2; 47 | string name = 3; 48 | string surname = 4; 49 | int32 age = 5; 50 | } 51 | 52 | message LoginReply { 53 | string token = 1; 54 | } 55 | 56 | message RegisterReply { 57 | string reply = 1; 58 | } 59 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | # Thunder CLI 🚀 2 | 3 | The Thunder CLI is a dedicated command‐line tool designed to work with the Thunder backend framework. It provides developers with a streamlined way to generate code, manage configurations, and interact with various Thunder features directly from the terminal. 4 | 5 | A custom CLI tool to automate: 6 | - **Generating gRPC and Prisma files** (`thunder generate`) 7 | - **Deploying Kubernetes resources** (`thunder deploy`) 8 | - **Initializing project** (`thunder init`) 9 | - **Docker** (`thunder build`) 10 | - **Test**: (`thunder test`) 11 | 12 | ## Installation 13 | 14 | ### 1. Clone or Download the Repository 15 | If you haven't already, navigate to your project directory where `generator.go` is located. 16 | 17 | ### 2. Run the Installation Script 18 | Make sure you have **Go**, **Minikube**, and **kubectl** installed. 19 | 20 | Run the following command: 21 | 22 | ```bash 23 | chmod +x install.sh && ./install.sh 24 | ``` 25 | 26 | This script will: 27 | - Compile `generator.go` into the `thunder-generate` binary. 28 | - Move `thunder-generate` and the `thunder` CLI script to `/usr/local/bin/`. 29 | - Make them globally accessible. 30 | 31 | ## Usage 32 | 33 | ### Generate gRPC & Prisma Files 34 | ```bash 35 | thunder generate --proto yourfile.proto 36 | ``` 37 | 38 | ### Test application 39 | ```bash 40 | thunder test 41 | ``` 42 | 43 | ### Generate project 44 | ``` 45 | thunder init projectname 46 | ``` 47 | > **Note** replace projectname with actual project name 48 | 49 | ### Deploy Kubernetes Resources 50 | Before deploying make sure You run that command: 51 | ``` 52 | thunder build 53 | ``` 54 | 55 | Congratulations!, Now You can use deploy! 56 | ```bash 57 | thunder deploy 58 | ``` 59 | 60 | This command will: 61 | 1. Start Minikube. 62 | 2. Apply PostgreSQL deployments and services. 63 | 3. Apply your app’s Kubernetes deployments and services. 64 | 4. Restart PgBouncer and your app deployment. 65 | 5. Forward port `8080` to access the application. 66 | 67 | 68 | ## Requirements 69 | - **Go** (for building `thunder-generate`) 70 | - **Minikube** (for Kubernetes) 71 | - **kubectl** (to manage Kubernetes resources) 72 | - **Prisma Client Go** (if using Prisma) 73 | - **Protobuf Compiler (`protoc`)** (if using gRPC) 74 | 75 | ## Troubleshooting 76 | - If `thunder` is not recognized, make sure `/usr/local/bin/` is in your `$PATH`: 77 | ```bash 78 | export PATH=$PATH:/usr/local/bin 79 | ``` 80 | -------------------------------------------------------------------------------- /cmd/app/client/go.mod: -------------------------------------------------------------------------------- 1 | module client 2 | 3 | go 1.23.0 4 | 5 | require google.golang.org/grpc v1.71.0 6 | 7 | require ( 8 | golang.org/x/net v0.34.0 // indirect 9 | golang.org/x/sys v0.29.0 // indirect 10 | golang.org/x/text v0.21.0 // indirect 11 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 12 | google.golang.org/protobuf v1.36.4 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /cmd/app/client/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 2 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 4 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 5 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 6 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 12 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 13 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 14 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 15 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 16 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 17 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 18 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 19 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 20 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 21 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 22 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 23 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 24 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 25 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 26 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 27 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 28 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 29 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 30 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 31 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 32 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 33 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 34 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 35 | -------------------------------------------------------------------------------- /cmd/app/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | . "generated" 9 | "io/ioutil" 10 | "log" 11 | 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials" 14 | "google.golang.org/grpc/metadata" 15 | ) 16 | 17 | func main() { 18 | 19 | serverCert, err := ioutil.ReadFile("../../certs/server.crt") 20 | if err != nil { 21 | log.Fatalf("could not read server certificate: %v", err) 22 | } 23 | 24 | // Create a certificate pool from the server certificate. 25 | certPool := x509.NewCertPool() 26 | if ok := certPool.AppendCertsFromPEM(serverCert); !ok { 27 | log.Fatal("failed to append server certificate") 28 | } 29 | 30 | // Set up TLS configuration with the certificate pool and enforce TLS 1.2 or higher. 31 | tlsConfig := &tls.Config{ 32 | RootCAs: certPool, 33 | MinVersion: tls.VersionTLS12, 34 | } 35 | tlsCreds := credentials.NewTLS(tlsConfig) 36 | 37 | // Dial the gRPC server using TLS credentials. 38 | conn, err := grpc.Dial("localhost:50051", 39 | grpc.WithTransportCredentials(tlsCreds), 40 | grpc.WithBlock(), 41 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*5)), 42 | ) 43 | if err != nil { 44 | log.Fatalf("did not connect: %v", err) 45 | } 46 | defer conn.Close() 47 | 48 | client := NewAuthClient(conn) 49 | ctx := context.Background() 50 | registerReply, err := client.Register(ctx, &RegisterRequest{ 51 | Email: "kmosc1211@example.com", 52 | Password: "password", 53 | Name: "Kamil", 54 | Surname: "Mosciszko", 55 | Age: 27, 56 | }) 57 | if err != nil { 58 | panic(err) 59 | } 60 | fmt.Println("Received registration response:", registerReply) 61 | 62 | loginReply, err := client.Login(ctx, &LoginRequest{ 63 | Email: "kmosc1211@example.com", 64 | Password: "password", 65 | }) 66 | if err != nil { 67 | log.Fatalf("Login failed: %v", err) 68 | } 69 | 70 | token := loginReply.Token 71 | fmt.Println("Received JWT token:", token) 72 | md := metadata.Pairs("authorization", token) 73 | outgoingCtx := metadata.NewOutgoingContext(ctx, md) 74 | protectedReply, err := client.SampleProtected(outgoingCtx, &ProtectedRequest{ 75 | Text: "Hello from client", 76 | }) 77 | if err != nil { 78 | log.Fatalf("SampleProtected failed: %v", err) 79 | } 80 | fmt.Println("SampleProtected response:", protectedReply.Result) 81 | } 82 | -------------------------------------------------------------------------------- /cmd/app/server/audit.log: -------------------------------------------------------------------------------- 1 | 2025-03-10T21:58:41+01:00 - [::1]:47052 POST /v1/auth/login 2 | 3 | 2025-03-10T21:59:02+01:00 - [::1]:55344 POST /v1/auth/login 4 | 5 | 2025-03-10T21:59:03+01:00 - [::1]:55348 POST /v1/auth/login 6 | 7 | -------------------------------------------------------------------------------- /cmd/app/server/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 7 | github.com/opentracing/opentracing-go v1.2.0 8 | github.com/spf13/viper v1.20.0 9 | github.com/uber/jaeger-client-go v2.30.0+incompatible 10 | github.com/valyala/fasthttp v1.59.0 11 | go.uber.org/zap v1.27.0 12 | google.golang.org/grpc v1.71.0 13 | ) 14 | 15 | require ( 16 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 17 | github.com/andybalholm/brotli v1.1.1 // indirect 18 | github.com/fsnotify/fsnotify v1.8.0 // indirect 19 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 20 | github.com/golang/protobuf v1.5.4 // indirect 21 | github.com/google/go-cmp v0.7.0 // indirect 22 | github.com/klauspost/compress v1.17.11 // indirect 23 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 24 | github.com/pkg/errors v0.9.1 // indirect 25 | github.com/sagikazarmark/locafero v0.7.0 // indirect 26 | github.com/sourcegraph/conc v0.3.0 // indirect 27 | github.com/spf13/afero v1.12.0 // indirect 28 | github.com/spf13/cast v1.7.1 // indirect 29 | github.com/spf13/pflag v1.0.6 // indirect 30 | github.com/subosito/gotenv v1.6.0 // indirect 31 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 32 | github.com/valyala/bytebufferpool v1.0.0 // indirect 33 | go.uber.org/atomic v1.9.0 // indirect 34 | go.uber.org/multierr v1.10.0 // indirect 35 | golang.org/x/net v0.35.0 // indirect 36 | golang.org/x/sys v0.30.0 // indirect 37 | golang.org/x/text v0.22.0 // indirect 38 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect 39 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect 40 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect 41 | google.golang.org/protobuf v1.36.5 // indirect 42 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /cmd/app/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "db" 5 | "fmt" 6 | "io" 7 | "log" 8 | "middlewares" 9 | "net" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | 15 | . "routes" 16 | 17 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 18 | "github.com/opentracing/opentracing-go" 19 | "github.com/spf13/viper" 20 | "github.com/uber/jaeger-client-go" 21 | "github.com/uber/jaeger-client-go/config" 22 | "github.com/valyala/fasthttp" 23 | "github.com/valyala/fasthttp/fasthttpadaptor" 24 | "go.uber.org/zap" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/credentials" 27 | ) 28 | 29 | func initConfig() { 30 | viper.SetDefault("grpc.port", ":50051") 31 | viper.SetDefault("http.port", ":8080") 32 | viper.AutomaticEnv() 33 | } 34 | 35 | // initJaeger initializes a Jaeger tracer. 36 | func initJaeger(service string) (opentracing.Tracer, io.Closer) { 37 | cfg, err := config.FromEnv() 38 | if err != nil { 39 | log.Fatalf("Failed to read Jaeger env vars: %v", err) 40 | } 41 | cfg.ServiceName = service 42 | tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger)) 43 | if err != nil { 44 | log.Fatalf("Could not initialize Jaeger tracer: %v", err) 45 | } 46 | opentracing.SetGlobalTracer(tracer) 47 | return tracer, closer 48 | } 49 | 50 | type App struct { 51 | certFile string 52 | keyFile string 53 | db *db.PrismaClient 54 | grpcServer *grpc.Server 55 | logger *zap.SugaredLogger 56 | gwmux *runtime.ServeMux 57 | } 58 | 59 | func NewApp() (*App, error) { 60 | logger, err := zap.NewProduction() 61 | if err != nil { 62 | log.Fatal(err) 63 | return nil, err 64 | } 65 | 66 | certFile := "../../certs/server.crt" 67 | keyFile := "../../certs/server.key" 68 | sugar := logger.Sugar() 69 | creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) 70 | if err != nil { 71 | sugar.Fatalf("Failed to load TLS credentials: %v", err) 72 | return nil, err 73 | } 74 | rateLimiter := middlewares.NewRateLimiter(5, 10) 75 | 76 | // Create the gRPC server with TLS and middleware. 77 | grpcServer := grpc.NewServer( 78 | grpc.Creds(creds), 79 | grpc.UnaryInterceptor( 80 | middlewares.ChainUnaryInterceptors( 81 | rateLimiter.RateLimiterInterceptor, 82 | middlewares.AuthUnaryInterceptor, 83 | ), 84 | ), 85 | ) 86 | 87 | return &App{ 88 | certFile: certFile, 89 | keyFile: keyFile, 90 | db: db.NewClient(), 91 | grpcServer: grpcServer, 92 | logger: sugar, 93 | gwmux: runtime.NewServeMux(), 94 | }, nil 95 | } 96 | 97 | func (app *App) RegisterMux() fasthttp.RequestHandler { 98 | // fasthttp handler 99 | fasthttpHandler := fasthttpadaptor.NewFastHTTPHandler(app.gwmux) 100 | 101 | // Define FastHTTP handlers. 102 | healthCheckHandler := func(ctx *fasthttp.RequestCtx) { 103 | ctx.SetStatusCode(fasthttp.StatusOK) 104 | ctx.SetBody([]byte("OK")) 105 | } 106 | 107 | readyCheckHandler := func(ctx *fasthttp.RequestCtx) { 108 | ctx.SetStatusCode(fasthttp.StatusOK) 109 | ctx.SetBody([]byte("Ready")) 110 | } 111 | 112 | // Create a FastHTTP router. 113 | fastMux := middlewares.CORSMiddleware(middlewares.LoggingMiddleware(func(ctx *fasthttp.RequestCtx) { 114 | switch string(ctx.Path()) { 115 | case "/health": 116 | healthCheckHandler(ctx) 117 | case "/ready": 118 | readyCheckHandler(ctx) 119 | default: 120 | fasthttpHandler(ctx) // Pass other requests to gRPC-Gateway 121 | } 122 | })) 123 | return fastMux 124 | } 125 | 126 | // running 127 | func (app *App) Run() error { 128 | grpcPort := viper.GetString("grpc.port") 129 | lis, err := net.Listen("tcp", grpcPort) 130 | if err != nil { 131 | log.Fatalln("Failed to listen:", err) 132 | } 133 | if err := app.db.Prisma.Connect(); err != nil { 134 | panic(err) 135 | } 136 | defer func() { 137 | if err := app.db.Prisma.Disconnect(); err != nil { 138 | panic(err) 139 | } 140 | }() 141 | 142 | // Register gRPC services before starting the server. 143 | RegisterServers(app.grpcServer, app.db, app.logger) 144 | 145 | log.Println(fmt.Sprintf("Starting gRPC server on port %s", grpcPort)) 146 | // Run gRPC server in a separate goroutine. 147 | go func() { 148 | if err := app.grpcServer.Serve(lis); err != nil { 149 | app.logger.Errorf("gRPC server stopped: %v", err) 150 | } 151 | }() 152 | 153 | clientCreds, err := credentials.NewClientTLSFromFile(app.certFile, "localhost") 154 | if err != nil { 155 | app.logger.Fatalf("Failed to load client TLS credentials: %v", err) 156 | } 157 | conn, err := grpc.Dial("localhost"+grpcPort, grpc.WithTransportCredentials(clientCreds)) 158 | if err != nil { 159 | log.Fatalln("Failed to dial gRPC server:", err) 160 | return err 161 | } 162 | 163 | // Register gRPC-Gateway handlers. 164 | RegisterHandlers(app.gwmux, conn) 165 | 166 | // Convert the gRPC-Gateway mux to work with fasthttp. 167 | 168 | // Setup FastHTTP server. 169 | httpPort := viper.GetString("http.port") 170 | log.Println(fmt.Sprintf("Starting REST gateway on port %s", httpPort)) 171 | httpServer := &fasthttp.Server{ 172 | Handler: app.RegisterMux(), 173 | ReadTimeout: 5 * time.Second, 174 | WriteTimeout: 10 * time.Second, 175 | IdleTimeout: 60 * time.Second, 176 | } 177 | log.Println("\033[32m✓ Server is running!\033[0m") 178 | 179 | // Run FastHTTP server in a separate goroutine. 180 | go func() { 181 | if err := httpServer.ListenAndServeTLS(httpPort, app.certFile, app.keyFile); err != nil { 182 | app.logger.Errorf("FastHTTP server stopped: %v", err) 183 | } 184 | }() 185 | 186 | // Listen for interrupt or termination signals. 187 | quit := make(chan os.Signal, 1) 188 | signal.Notify(quit, os.Interrupt, syscall.SIGTERM) 189 | <-quit 190 | app.logger.Info("Shutdown signal received. Initiating graceful shutdown...") 191 | 192 | // Gracefully stop the gRPC server. 193 | app.grpcServer.GracefulStop() 194 | app.logger.Info("gRPC server gracefully stopped.") 195 | // Gracefully shutdown the FastHTTP server. 196 | if err := httpServer.Shutdown(); err != nil { 197 | app.logger.Errorf("Error shutting down FastHTTP server: %v", err) 198 | } else { 199 | app.logger.Info("FastHTTP server gracefully stopped.") 200 | } 201 | return nil 202 | } 203 | 204 | // main program 205 | func main() { 206 | initConfig() 207 | initJaeger("grpc-gateway") 208 | app, err := NewApp() 209 | if err != nil { 210 | panic(err) 211 | } 212 | app.Run() 213 | } 214 | -------------------------------------------------------------------------------- /cmd/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.22.2 4 | 5 | require google.golang.org/protobuf v1.35.1 // indirect 6 | -------------------------------------------------------------------------------- /cmd/go.sum: -------------------------------------------------------------------------------- 1 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 2 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 3 | -------------------------------------------------------------------------------- /cmd/protoc-gen-rpc-impl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "google.golang.org/protobuf/compiler/protogen" 10 | ) 11 | 12 | func main() { 13 | protogen.Options{}.Run(func(gen *protogen.Plugin) error { 14 | for _, f := range gen.Files { 15 | if !f.Generate { 16 | continue 17 | } 18 | if err := generateFile(gen, f); err != nil { 19 | return err 20 | } 21 | } 22 | return nil 23 | }) 24 | } 25 | 26 | func generateFile(gen *protogen.Plugin, file *protogen.File) error { 27 | if len(file.Services) == 0 { 28 | return nil 29 | } 30 | filenameWithoutExt := strings.TrimSuffix(string(*file.Proto.Name), ".proto") 31 | // Build the filename where generated code will be merged 32 | filename := "./pkg/services/" + filenameWithoutExt + "_server.go" 33 | 34 | // Generate new content using a string builder 35 | var sb strings.Builder 36 | sb.WriteString("package services\n\n") 37 | sb.WriteString("import (\n") 38 | sb.WriteString(` . "generated"` + "\n") 39 | sb.WriteString(` "context"` + "\n") 40 | sb.WriteString(` "db"` + "\n") 41 | sb.WriteString(` "go.uber.org/zap"` + "\n") 42 | sb.WriteString(")\n\n") 43 | 44 | // Generate service implementations for each service in the proto file 45 | for _, service := range file.Services { 46 | generateServiceImplementation(&sb, service) 47 | } 48 | 49 | newContent := sb.String() 50 | 51 | // Merge the new content with any existing file content 52 | mergedContent, err := mergeWithExisting(filename, newContent) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // Write the merged content to the file 58 | return os.WriteFile(filename, []byte(mergedContent), 0644) 59 | } 60 | 61 | func generateServiceImplementation(sb *strings.Builder, service *protogen.Service) { 62 | structName := service.GoName + "ServiceServer" 63 | 64 | // Generate the service struct 65 | sb.WriteString("type " + structName + " struct {\n") 66 | sb.WriteString(" Unimplemented" + service.GoName + "Server\n") 67 | sb.WriteString(" PrismaClient *db.PrismaClient\n") 68 | sb.WriteString(" Logger *zap.SugaredLogger\n") 69 | sb.WriteString("}\n\n") 70 | 71 | // Generate a constructor 72 | sb.WriteString("func New" + structName + "() *" + structName + " {\n") 73 | sb.WriteString(" return &" + structName + "{}\n") 74 | sb.WriteString("}\n\n") 75 | 76 | // Generate methods for each RPC 77 | for _, method := range service.Methods { 78 | generateMethodImplementation(sb, method, structName) 79 | } 80 | } 81 | 82 | func generateMethodImplementation(sb *strings.Builder, method *protogen.Method, structName string) { 83 | methodName := method.GoName 84 | inputType := method.Input.GoIdent.GoName 85 | outputType := method.Output.GoIdent.GoName 86 | 87 | signature := fmt.Sprintf("func (s *%s) %s(ctx context.Context, req *%s) (*%s, error) {", 88 | structName, methodName, inputType, outputType) 89 | sb.WriteString(signature + "\n") 90 | sb.WriteString(" // TODO: Implement " + methodName + "\n") 91 | sb.WriteString(" return &" + outputType + "{}, nil\n") 92 | sb.WriteString("}\n\n") 93 | } 94 | 95 | // mergeWithExisting reads an existing file (if any) and merges it with newContent, 96 | // avoiding duplicate method definitions. 97 | func mergeWithExisting(filename, newContent string) (string, error) { 98 | existing, err := os.ReadFile(filename) 99 | if err != nil { 100 | // If the file doesn't exist, just return the new content. 101 | return newContent, nil 102 | } 103 | merged := mergeContent(string(existing), newContent) 104 | return merged, nil 105 | } 106 | 107 | // mergeContent uses a simple strategy: it identifies method definitions by looking for 108 | // "func (s *..." signatures and then only appends methods that are not already present. 109 | func mergeContent(existing, new string) string { 110 | // Regular expression to capture service struct and method names. 111 | methodRegex := regexp.MustCompile(`func \(s \*([A-Za-z0-9_]+)\) ([A-Za-z0-9_]+)\(`) 112 | existingMethods := make(map[string]bool) 113 | for _, match := range methodRegex.FindAllStringSubmatch(existing, -1) { 114 | if len(match) >= 3 { 115 | key := match[1] + "::" + match[2] 116 | existingMethods[key] = true 117 | } 118 | } 119 | 120 | // Break new content into lines and extract method blocks. 121 | lines := strings.Split(new, "\n") 122 | var methodsToAdd []string 123 | var currentMethod []string 124 | inMethod := false 125 | for _, line := range lines { 126 | if strings.HasPrefix(line, "func (s *") { 127 | if inMethod && len(currentMethod) > 0 { 128 | methodBlock := strings.Join(currentMethod, "\n") 129 | m := methodRegex.FindStringSubmatch(currentMethod[0]) 130 | if len(m) >= 3 { 131 | key := m[1] + "::" + m[2] 132 | if !existingMethods[key] { 133 | methodsToAdd = append(methodsToAdd, methodBlock) 134 | } 135 | } 136 | } 137 | inMethod = true 138 | currentMethod = []string{line} 139 | } else if inMethod { 140 | currentMethod = append(currentMethod, line) 141 | if line == "" { 142 | // End of a method block 143 | methodBlock := strings.Join(currentMethod, "\n") 144 | m := methodRegex.FindStringSubmatch(currentMethod[0]) 145 | if len(m) >= 3 { 146 | key := m[1] + "::" + m[2] 147 | if !existingMethods[key] { 148 | methodsToAdd = append(methodsToAdd, methodBlock) 149 | } 150 | } 151 | inMethod = false 152 | currentMethod = nil 153 | } 154 | } 155 | } 156 | // In case a method was being collected but not closed by an empty line 157 | if inMethod && len(currentMethod) > 0 { 158 | m := methodRegex.FindStringSubmatch(currentMethod[0]) 159 | if len(m) >= 3 { 160 | key := m[1] + "::" + m[2] 161 | if !existingMethods[key] { 162 | methodsToAdd = append(methodsToAdd, strings.Join(currentMethod, "\n")) 163 | } 164 | } 165 | } 166 | 167 | // For simplicity, we assume that the existing file has a header (package and import sections) 168 | // and then the method definitions. Here we simply append any new methods to the end. 169 | merged := existing + "\n\n" + strings.Join(methodsToAdd, "\n\n") 170 | return merged 171 | } 172 | -------------------------------------------------------------------------------- /coverage.txt: -------------------------------------------------------------------------------- 1 | mode: set 2 | main/generator.go:22.63,24.16 2 0 3 | main/generator.go:24.16,26.3 1 0 4 | main/generator.go:27.2,29.22 3 0 5 | main/generator.go:69.52,79.2 4 0 6 | main/generator.go:82.47,84.16 2 0 7 | main/generator.go:84.16,86.3 1 0 8 | main/generator.go:88.2,89.16 2 0 9 | main/generator.go:89.16,91.3 1 0 10 | main/generator.go:92.2,95.16 3 0 11 | main/generator.go:95.16,97.3 1 0 12 | main/generator.go:98.2,98.83 1 0 13 | main/generator.go:101.69,107.2 5 0 14 | main/generator.go:110.13,117.18 5 0 15 | main/generator.go:117.18,130.17 1 0 16 | main/generator.go:130.17,132.4 1 0 17 | main/generator.go:134.3,134.80 1 0 18 | main/generator.go:138.2,138.13 1 0 19 | main/generator.go:138.13,139.118 1 0 20 | main/generator.go:139.118,141.4 1 0 21 | main/generator.go:142.3,142.62 1 0 22 | main/generator.go:146.2,146.15 1 0 23 | main/generator.go:146.15,148.17 2 0 24 | main/generator.go:148.17,150.4 1 0 25 | main/generator.go:151.3,151.33 1 0 26 | main/pkg/app/client/main.go:17.13,20.16 2 0 27 | main/pkg/app/client/main.go:20.16,22.3 1 0 28 | main/pkg/app/client/main.go:25.2,26.56 2 0 29 | main/pkg/app/client/main.go:26.56,28.3 1 0 30 | main/pkg/app/client/main.go:31.2,43.16 4 0 31 | main/pkg/app/client/main.go:43.16,45.3 1 0 32 | main/pkg/app/client/main.go:46.2,57.16 5 0 33 | main/pkg/app/client/main.go:57.16,58.13 1 0 34 | main/pkg/app/client/main.go:60.2,66.16 3 0 35 | main/pkg/app/client/main.go:66.16,68.3 1 0 36 | main/pkg/app/client/main.go:70.2,77.16 6 0 37 | main/pkg/app/client/main.go:77.16,79.3 1 0 38 | main/pkg/app/client/main.go:80.2,80.65 1 0 39 | main/pkg/app/server/main.go:27.19,31.2 3 0 40 | main/pkg/app/server/main.go:34.65,36.16 2 0 41 | main/pkg/app/server/main.go:36.16,38.3 1 0 42 | main/pkg/app/server/main.go:39.2,41.16 3 0 43 | main/pkg/app/server/main.go:41.16,43.3 1 0 44 | main/pkg/app/server/main.go:44.2,45.23 2 0 45 | main/pkg/app/server/main.go:48.13,53.16 3 0 46 | main/pkg/app/server/main.go:53.16,55.3 1 0 47 | main/pkg/app/server/main.go:56.2,67.16 8 0 48 | main/pkg/app/server/main.go:67.16,69.3 1 0 49 | main/pkg/app/server/main.go:72.2,74.16 3 0 50 | main/pkg/app/server/main.go:74.16,76.3 1 0 51 | main/pkg/app/server/main.go:79.2,80.48 2 0 52 | main/pkg/app/server/main.go:80.48,81.13 1 0 53 | main/pkg/app/server/main.go:83.2,83.15 1 0 54 | main/pkg/app/server/main.go:83.15,84.52 1 0 55 | main/pkg/app/server/main.go:84.52,85.14 1 0 56 | main/pkg/app/server/main.go:90.2,110.12 8 0 57 | main/pkg/app/server/main.go:110.12,112.3 1 0 58 | main/pkg/app/server/main.go:115.2,116.16 2 0 59 | main/pkg/app/server/main.go:116.16,118.3 1 0 60 | main/pkg/app/server/main.go:119.2,120.16 2 0 61 | main/pkg/app/server/main.go:120.16,122.3 1 0 62 | main/pkg/app/server/main.go:125.2,132.55 4 0 63 | main/pkg/app/server/main.go:132.55,135.3 2 0 64 | main/pkg/app/server/main.go:137.2,137.54 1 0 65 | main/pkg/app/server/main.go:137.54,140.3 2 0 66 | main/pkg/app/server/main.go:143.2,143.101 1 0 67 | main/pkg/app/server/main.go:143.101,144.29 1 0 68 | main/pkg/app/server/main.go:145.18,146.27 1 0 69 | main/pkg/app/server/main.go:147.17,148.26 1 0 70 | main/pkg/app/server/main.go:149.11,150.24 1 0 71 | main/pkg/app/server/main.go:155.2,157.89 3 0 72 | main/pkg/app/server/main.go:157.89,159.3 1 0 73 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "Running Prisma migrations..." 5 | cd pkg && prisma-client-go db push 6 | 7 | echo "Starting application..." 8 | cd .. 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "text/template" 11 | ) 12 | 13 | // Service holds the metadata needed for generating the register functions. 14 | type Service struct { 15 | ServiceName string `json:"ServiceName"` 16 | ServiceStruct string `json:"ServiceStruct"` 17 | ServiceRegister string `json:"ServiceRegister"` 18 | HandlerRegister string `json:"HandlerRegister"` 19 | } 20 | 21 | // loadServicesFromJSON reads the service definitions from a JSON file. 22 | func loadServicesFromJSON(filepath string) ([]Service, error) { 23 | data, err := os.ReadFile(filepath) 24 | if err != nil { 25 | return nil, err 26 | } 27 | var services []Service 28 | err = json.Unmarshal(data, &services) 29 | return services, err 30 | } 31 | 32 | // Template for RegisterServers and RegisterHandlers. 33 | const templateCode = `package routes 34 | 35 | import ( 36 | "context" 37 | "log" 38 | . "generated" 39 | 40 | "google.golang.org/grpc" 41 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 42 | "go.uber.org/zap" 43 | "db" 44 | pb "services" 45 | ) 46 | 47 | // RegisterServers registers gRPC services to the server. 48 | func RegisterServers(server *grpc.Server, client *db.PrismaClient, sugar *zap.SugaredLogger) { 49 | {{range .}} 50 | {{.ServiceRegister}}(server, &pb.{{.ServiceStruct}}{ 51 | PrismaClient: client, 52 | Logger: sugar, 53 | }) 54 | {{end}} 55 | } 56 | 57 | // RegisterHandlers registers gRPC-Gateway handlers. 58 | func RegisterHandlers(gwmux *runtime.ServeMux, conn *grpc.ClientConn) { 59 | var err error 60 | {{range .}} 61 | err = {{.HandlerRegister}}(context.Background(), gwmux, conn) 62 | if err != nil { 63 | log.Fatalln("Failed to register gateway:", err) 64 | } 65 | {{end}} 66 | } 67 | ` 68 | 69 | func runCommand(name string, args ...string) error { 70 | // Create the command 71 | cmd := exec.Command(name, args...) 72 | 73 | // Set the command's output to log to the console 74 | cmd.Stdout = os.Stdout 75 | cmd.Stderr = os.Stderr 76 | 77 | // Run the command 78 | return cmd.Run() 79 | } 80 | 81 | // generateRegisterFile creates the register file dynamically 82 | func generateRegisterFile(services []Service) { 83 | tmpl, err := template.New("register").Parse(templateCode) 84 | if err != nil { 85 | log.Fatalf("Error parsing template: %v", err) 86 | } 87 | 88 | file, err := os.Create("pkg/routes/generated_register.go") 89 | if err != nil { 90 | log.Fatalf("Error creating file: %v", err) 91 | } 92 | defer file.Close() 93 | 94 | err = tmpl.Execute(file, services) 95 | if err != nil { 96 | log.Fatalf("Error executing template: %v", err) 97 | } 98 | fmt.Println("Generated register file: pkg/internal/routes/generated_register.go") 99 | } 100 | 101 | func runCommandInDir(dir string, name string, args ...string) error { 102 | cmd := exec.Command(name, args...) 103 | cmd.Dir = dir // Ustawienie katalogu roboczego 104 | cmd.Stdout = os.Stdout 105 | cmd.Stderr = os.Stderr 106 | return cmd.Run() 107 | } 108 | 109 | // It generates proto files and builds from Prisma schema 110 | func main() { 111 | proto := flag.String("proto", "", "Path to the .proto file") 112 | prisma := flag.Bool("prisma", true, "Whether to run the Prisma db push command") 113 | generate := flag.Bool("generate", true, "Whether to generate register functions") 114 | flag.Parse() 115 | 116 | // First command: Run protoc to generate Go code from .proto file 117 | if *proto != "" { 118 | if err := runCommand("protoc", 119 | "-I", ".", 120 | "--go_out=./pkg/services/generated", 121 | "--go_opt=paths=source_relative", 122 | "--go-grpc_out=./pkg/services/generated", 123 | "--go-grpc_opt=paths=source_relative", 124 | "--grpc-gateway_out=./pkg/services/generated", 125 | "--grpc-gateway_opt=paths=source_relative", 126 | "--rpc-impl_out=./pkg/services", 127 | "--openapiv2_out=./pkg/services", 128 | "--openapiv2_opt=logtostderr=true", 129 | *proto, 130 | ); err != nil { 131 | log.Fatalf("Error executing protoc command: %v", err) 132 | } 133 | 134 | fmt.Println("Protobuf, gRPC, and gRPC Gateway files generated successfully!") 135 | } 136 | 137 | // Second command: Run Prisma command to push database changes 138 | if *prisma { 139 | if err := runCommandInDir("./pkg", "go", "run", "github.com/steebchen/prisma-client-go", "db", "push"); err != nil { 140 | log.Fatalf("Error executing Prisma command: %v", err) 141 | } 142 | fmt.Println("Prisma database changes pushed successfully!") 143 | } 144 | 145 | // Third step: Generate gRPC registration file 146 | if *generate { 147 | services, err := loadServicesFromJSON("services.json") 148 | if err != nil { 149 | log.Fatalf("Error loading services from JSON: %v", err) 150 | } 151 | generateRegisterFile(services) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 7 | github.com/opentracing/opentracing-go v1.2.0 8 | github.com/spf13/viper v1.19.0 9 | github.com/uber/jaeger-client-go v2.30.0+incompatible 10 | github.com/valyala/fasthttp v1.59.0 11 | go.uber.org/zap v1.27.0 12 | google.golang.org/grpc v1.71.0 13 | ) 14 | 15 | require ( 16 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 17 | github.com/andybalholm/brotli v1.1.1 // indirect 18 | github.com/fsnotify/fsnotify v1.7.0 // indirect 19 | github.com/hashicorp/hcl v1.0.0 // indirect 20 | github.com/klauspost/compress v1.17.11 // indirect 21 | github.com/magiconair/properties v1.8.7 // indirect 22 | github.com/mitchellh/mapstructure v1.5.0 // indirect 23 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 24 | github.com/pkg/errors v0.9.1 // indirect 25 | github.com/sagikazarmark/locafero v0.4.0 // indirect 26 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 27 | github.com/sourcegraph/conc v0.3.0 // indirect 28 | github.com/spf13/afero v1.11.0 // indirect 29 | github.com/spf13/cast v1.6.0 // indirect 30 | github.com/spf13/pflag v1.0.5 // indirect 31 | github.com/stretchr/testify v1.10.0 // indirect 32 | github.com/subosito/gotenv v1.6.0 // indirect 33 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 34 | github.com/valyala/bytebufferpool v1.0.0 // indirect 35 | go.uber.org/atomic v1.9.0 // indirect 36 | go.uber.org/multierr v1.10.0 // indirect 37 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 38 | golang.org/x/net v0.35.0 // indirect 39 | golang.org/x/sys v0.30.0 // indirect 40 | golang.org/x/text v0.22.0 // indirect 41 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect 42 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect 43 | google.golang.org/protobuf v1.36.5 // indirect 44 | gopkg.in/ini.v1 v1.67.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= 4 | github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= 5 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 6 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 7 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 8 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 14 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 15 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 18 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 19 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 20 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 22 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 23 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 24 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 25 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 26 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 28 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 29 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 30 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 31 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 32 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 33 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 34 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 35 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 36 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 37 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 38 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 39 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 40 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 41 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 42 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 43 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 44 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 45 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 46 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 47 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 48 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 49 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 50 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 51 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 52 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 53 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 57 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 59 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 60 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 61 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 62 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 63 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 64 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 65 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 66 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 67 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 68 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 69 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 70 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 71 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 72 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 73 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 76 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 77 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 78 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 79 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 80 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 83 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 84 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 85 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 86 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 87 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 88 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 89 | github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= 90 | github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 91 | github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= 92 | github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 93 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 94 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 95 | github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= 96 | github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= 97 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 98 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 99 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 100 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 101 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 102 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 103 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 104 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 105 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 106 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 107 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 108 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 109 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 110 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 111 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 112 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 113 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 114 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 115 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 116 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 117 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 118 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 119 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 120 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 121 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 122 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 123 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 124 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 125 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 126 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 127 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 128 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 129 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 130 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 131 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 132 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 133 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 134 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 135 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 136 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 137 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 142 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 143 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 144 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 145 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 146 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 147 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 148 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 149 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 152 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 153 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 154 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 155 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 156 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= 157 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= 158 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= 159 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 160 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 161 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 162 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 163 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 166 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 167 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 168 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 169 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 170 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 171 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 172 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 173 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 174 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.0 2 | 3 | toolchain go1.23.0 4 | 5 | use ( 6 | ./cmd/app/server 7 | ./cmd/app/client 8 | ./ 9 | ./pkg/db 10 | ./pkg/services 11 | ./pkg/services/generated 12 | ./pkg/middlewares 13 | ./pkg/routes 14 | ) 15 | -------------------------------------------------------------------------------- /google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /google/api/field_behavior.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/protobuf/descriptor.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "FieldBehaviorProto"; 24 | option java_package = "com.google.api"; 25 | option objc_class_prefix = "GAPI"; 26 | 27 | extend google.protobuf.FieldOptions { 28 | // A designation of a specific field behavior (required, output only, etc.) 29 | // in protobuf messages. 30 | // 31 | // Examples: 32 | // 33 | // string name = 1 [(google.api.field_behavior) = REQUIRED]; 34 | // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; 35 | // google.protobuf.Duration ttl = 1 36 | // [(google.api.field_behavior) = INPUT_ONLY]; 37 | // google.protobuf.Timestamp expire_time = 1 38 | // [(google.api.field_behavior) = OUTPUT_ONLY, 39 | // (google.api.field_behavior) = IMMUTABLE]; 40 | repeated google.api.FieldBehavior field_behavior = 1052 [packed = false]; 41 | } 42 | 43 | // An indicator of the behavior of a given field (for example, that a field 44 | // is required in requests, or given as output but ignored as input). 45 | // This **does not** change the behavior in protocol buffers itself; it only 46 | // denotes the behavior and may affect how API tooling handles the field. 47 | // 48 | // Note: This enum **may** receive new values in the future. 49 | enum FieldBehavior { 50 | // Conventional default for enums. Do not use this. 51 | FIELD_BEHAVIOR_UNSPECIFIED = 0; 52 | 53 | // Specifically denotes a field as optional. 54 | // While all fields in protocol buffers are optional, this may be specified 55 | // for emphasis if appropriate. 56 | OPTIONAL = 1; 57 | 58 | // Denotes a field as required. 59 | // This indicates that the field **must** be provided as part of the request, 60 | // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). 61 | REQUIRED = 2; 62 | 63 | // Denotes a field as output only. 64 | // This indicates that the field is provided in responses, but including the 65 | // field in a request does nothing (the server *must* ignore it and 66 | // *must not* throw an error as a result of the field's presence). 67 | OUTPUT_ONLY = 3; 68 | 69 | // Denotes a field as input only. 70 | // This indicates that the field is provided in requests, and the 71 | // corresponding field is not included in output. 72 | INPUT_ONLY = 4; 73 | 74 | // Denotes a field as immutable. 75 | // This indicates that the field may be set once in a request to create a 76 | // resource, but may not be changed thereafter. 77 | IMMUTABLE = 5; 78 | 79 | // Denotes that a (repeated) field is an unordered list. 80 | // This indicates that the service may provide the elements of the list 81 | // in any arbitrary order, rather than the order the user originally 82 | // provided. Additionally, the list's order may or may not be stable. 83 | UNORDERED_LIST = 6; 84 | 85 | // Denotes that this field returns a non-empty default value if not set. 86 | // This indicates that if the user provides the empty value in a request, 87 | // a non-empty value will be returned. The user will not be aware of what 88 | // non-empty value to expect. 89 | NON_EMPTY_DEFAULT = 7; 90 | 91 | // Denotes that the field in a resource (a message annotated with 92 | // google.api.resource) is used in the resource name to uniquely identify the 93 | // resource. For AIP-compliant APIs, this should only be applied to the 94 | // `name` field on the resource. 95 | // 96 | // This behavior should not be applied to references to other resources within 97 | // the message. 98 | // 99 | // The identifier field of resources often have different field behavior 100 | // depending on the request it is embedded in (e.g. for Create methods name 101 | // is optional and unused, while for Update methods it is required). Instead 102 | // of method-specific annotations, only `IDENTIFIER` is required. 103 | IDENTIFIER = 8; 104 | } 105 | -------------------------------------------------------------------------------- /google/api/http.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | option cc_enable_arenas = true; 20 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 21 | option java_multiple_files = true; 22 | option java_outer_classname = "HttpProto"; 23 | option java_package = "com.google.api"; 24 | option objc_class_prefix = "GAPI"; 25 | 26 | // Defines the HTTP configuration for an API service. It contains a list of 27 | // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method 28 | // to one or more HTTP REST API methods. 29 | message Http { 30 | // A list of HTTP configuration rules that apply to individual API methods. 31 | // 32 | // **NOTE:** All service configuration rules follow "last one wins" order. 33 | repeated HttpRule rules = 1; 34 | 35 | // When set to true, URL path parameters will be fully URI-decoded except in 36 | // cases of single segment matches in reserved expansion, where "%2F" will be 37 | // left encoded. 38 | // 39 | // The default behavior is to not decode RFC 6570 reserved characters in multi 40 | // segment matches. 41 | bool fully_decode_reserved_expansion = 2; 42 | } 43 | 44 | // gRPC Transcoding 45 | // 46 | // gRPC Transcoding is a feature for mapping between a gRPC method and one or 47 | // more HTTP REST endpoints. It allows developers to build a single API service 48 | // that supports both gRPC APIs and REST APIs. Many systems, including [Google 49 | // APIs](https://github.com/googleapis/googleapis), 50 | // [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC 51 | // Gateway](https://github.com/grpc-ecosystem/grpc-gateway), 52 | // and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature 53 | // and use it for large scale production services. 54 | // 55 | // `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies 56 | // how different portions of the gRPC request message are mapped to the URL 57 | // path, URL query parameters, and HTTP request body. It also controls how the 58 | // gRPC response message is mapped to the HTTP response body. `HttpRule` is 59 | // typically specified as an `google.api.http` annotation on the gRPC method. 60 | // 61 | // Each mapping specifies a URL path template and an HTTP method. The path 62 | // template may refer to one or more fields in the gRPC request message, as long 63 | // as each field is a non-repeated field with a primitive (non-message) type. 64 | // The path template controls how fields of the request message are mapped to 65 | // the URL path. 66 | // 67 | // Example: 68 | // 69 | // service Messaging { 70 | // rpc GetMessage(GetMessageRequest) returns (Message) { 71 | // option (google.api.http) = { 72 | // get: "/v1/{name=messages/*}" 73 | // }; 74 | // } 75 | // } 76 | // message GetMessageRequest { 77 | // string name = 1; // Mapped to URL path. 78 | // } 79 | // message Message { 80 | // string text = 1; // The resource content. 81 | // } 82 | // 83 | // This enables an HTTP REST to gRPC mapping as below: 84 | // 85 | // - HTTP: `GET /v1/messages/123456` 86 | // - gRPC: `GetMessage(name: "messages/123456")` 87 | // 88 | // Any fields in the request message which are not bound by the path template 89 | // automatically become HTTP query parameters if there is no HTTP request body. 90 | // For example: 91 | // 92 | // service Messaging { 93 | // rpc GetMessage(GetMessageRequest) returns (Message) { 94 | // option (google.api.http) = { 95 | // get:"/v1/messages/{message_id}" 96 | // }; 97 | // } 98 | // } 99 | // message GetMessageRequest { 100 | // message SubMessage { 101 | // string subfield = 1; 102 | // } 103 | // string message_id = 1; // Mapped to URL path. 104 | // int64 revision = 2; // Mapped to URL query parameter `revision`. 105 | // SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. 106 | // } 107 | // 108 | // This enables a HTTP JSON to RPC mapping as below: 109 | // 110 | // - HTTP: `GET /v1/messages/123456?revision=2&sub.subfield=foo` 111 | // - gRPC: `GetMessage(message_id: "123456" revision: 2 sub: 112 | // SubMessage(subfield: "foo"))` 113 | // 114 | // Note that fields which are mapped to URL query parameters must have a 115 | // primitive type or a repeated primitive type or a non-repeated message type. 116 | // In the case of a repeated type, the parameter can be repeated in the URL 117 | // as `...?param=A¶m=B`. In the case of a message type, each field of the 118 | // message is mapped to a separate parameter, such as 119 | // `...?foo.a=A&foo.b=B&foo.c=C`. 120 | // 121 | // For HTTP methods that allow a request body, the `body` field 122 | // specifies the mapping. Consider a REST update method on the 123 | // message resource collection: 124 | // 125 | // service Messaging { 126 | // rpc UpdateMessage(UpdateMessageRequest) returns (Message) { 127 | // option (google.api.http) = { 128 | // patch: "/v1/messages/{message_id}" 129 | // body: "message" 130 | // }; 131 | // } 132 | // } 133 | // message UpdateMessageRequest { 134 | // string message_id = 1; // mapped to the URL 135 | // Message message = 2; // mapped to the body 136 | // } 137 | // 138 | // The following HTTP JSON to RPC mapping is enabled, where the 139 | // representation of the JSON in the request body is determined by 140 | // protos JSON encoding: 141 | // 142 | // - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` 143 | // - gRPC: `UpdateMessage(message_id: "123456" message { text: "Hi!" })` 144 | // 145 | // The special name `*` can be used in the body mapping to define that 146 | // every field not bound by the path template should be mapped to the 147 | // request body. This enables the following alternative definition of 148 | // the update method: 149 | // 150 | // service Messaging { 151 | // rpc UpdateMessage(Message) returns (Message) { 152 | // option (google.api.http) = { 153 | // patch: "/v1/messages/{message_id}" 154 | // body: "*" 155 | // }; 156 | // } 157 | // } 158 | // message Message { 159 | // string message_id = 1; 160 | // string text = 2; 161 | // } 162 | // 163 | // 164 | // The following HTTP JSON to RPC mapping is enabled: 165 | // 166 | // - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` 167 | // - gRPC: `UpdateMessage(message_id: "123456" text: "Hi!")` 168 | // 169 | // Note that when using `*` in the body mapping, it is not possible to 170 | // have HTTP parameters, as all fields not bound by the path end in 171 | // the body. This makes this option more rarely used in practice when 172 | // defining REST APIs. The common usage of `*` is in custom methods 173 | // which don't use the URL at all for transferring data. 174 | // 175 | // It is possible to define multiple HTTP methods for one RPC by using 176 | // the `additional_bindings` option. Example: 177 | // 178 | // service Messaging { 179 | // rpc GetMessage(GetMessageRequest) returns (Message) { 180 | // option (google.api.http) = { 181 | // get: "/v1/messages/{message_id}" 182 | // additional_bindings { 183 | // get: "/v1/users/{user_id}/messages/{message_id}" 184 | // } 185 | // }; 186 | // } 187 | // } 188 | // message GetMessageRequest { 189 | // string message_id = 1; 190 | // string user_id = 2; 191 | // } 192 | // 193 | // This enables the following two alternative HTTP JSON to RPC mappings: 194 | // 195 | // - HTTP: `GET /v1/messages/123456` 196 | // - gRPC: `GetMessage(message_id: "123456")` 197 | // 198 | // - HTTP: `GET /v1/users/me/messages/123456` 199 | // - gRPC: `GetMessage(user_id: "me" message_id: "123456")` 200 | // 201 | // Rules for HTTP mapping 202 | // 203 | // 1. Leaf request fields (recursive expansion nested messages in the request 204 | // message) are classified into three categories: 205 | // - Fields referred by the path template. They are passed via the URL path. 206 | // - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They 207 | // are passed via the HTTP 208 | // request body. 209 | // - All other fields are passed via the URL query parameters, and the 210 | // parameter name is the field path in the request message. A repeated 211 | // field can be represented as multiple query parameters under the same 212 | // name. 213 | // 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL 214 | // query parameter, all fields 215 | // are passed via URL path and HTTP request body. 216 | // 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP 217 | // request body, all 218 | // fields are passed via URL path and URL query parameters. 219 | // 220 | // Path template syntax 221 | // 222 | // Template = "/" Segments [ Verb ] ; 223 | // Segments = Segment { "/" Segment } ; 224 | // Segment = "*" | "**" | LITERAL | Variable ; 225 | // Variable = "{" FieldPath [ "=" Segments ] "}" ; 226 | // FieldPath = IDENT { "." IDENT } ; 227 | // Verb = ":" LITERAL ; 228 | // 229 | // The syntax `*` matches a single URL path segment. The syntax `**` matches 230 | // zero or more URL path segments, which must be the last part of the URL path 231 | // except the `Verb`. 232 | // 233 | // The syntax `Variable` matches part of the URL path as specified by its 234 | // template. A variable template must not contain other variables. If a variable 235 | // matches a single path segment, its template may be omitted, e.g. `{var}` 236 | // is equivalent to `{var=*}`. 237 | // 238 | // The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` 239 | // contains any reserved character, such characters should be percent-encoded 240 | // before the matching. 241 | // 242 | // If a variable contains exactly one path segment, such as `"{var}"` or 243 | // `"{var=*}"`, when such a variable is expanded into a URL path on the client 244 | // side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The 245 | // server side does the reverse decoding. Such variables show up in the 246 | // [Discovery 247 | // Document](https://developers.google.com/discovery/v1/reference/apis) as 248 | // `{var}`. 249 | // 250 | // If a variable contains multiple path segments, such as `"{var=foo/*}"` 251 | // or `"{var=**}"`, when such a variable is expanded into a URL path on the 252 | // client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. 253 | // The server side does the reverse decoding, except "%2F" and "%2f" are left 254 | // unchanged. Such variables show up in the 255 | // [Discovery 256 | // Document](https://developers.google.com/discovery/v1/reference/apis) as 257 | // `{+var}`. 258 | // 259 | // Using gRPC API Service Configuration 260 | // 261 | // gRPC API Service Configuration (service config) is a configuration language 262 | // for configuring a gRPC service to become a user-facing product. The 263 | // service config is simply the YAML representation of the `google.api.Service` 264 | // proto message. 265 | // 266 | // As an alternative to annotating your proto file, you can configure gRPC 267 | // transcoding in your service config YAML files. You do this by specifying a 268 | // `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same 269 | // effect as the proto annotation. This can be particularly useful if you 270 | // have a proto that is reused in multiple services. Note that any transcoding 271 | // specified in the service config will override any matching transcoding 272 | // configuration in the proto. 273 | // 274 | // The following example selects a gRPC method and applies an `HttpRule` to it: 275 | // 276 | // http: 277 | // rules: 278 | // - selector: example.v1.Messaging.GetMessage 279 | // get: /v1/messages/{message_id}/{sub.subfield} 280 | // 281 | // Special notes 282 | // 283 | // When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the 284 | // proto to JSON conversion must follow the [proto3 285 | // specification](https://developers.google.com/protocol-buffers/docs/proto3#json). 286 | // 287 | // While the single segment variable follows the semantics of 288 | // [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String 289 | // Expansion, the multi segment variable **does not** follow RFC 6570 Section 290 | // 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion 291 | // does not expand special characters like `?` and `#`, which would lead 292 | // to invalid URLs. As the result, gRPC Transcoding uses a custom encoding 293 | // for multi segment variables. 294 | // 295 | // The path variables **must not** refer to any repeated or mapped field, 296 | // because client libraries are not capable of handling such variable expansion. 297 | // 298 | // The path variables **must not** capture the leading "/" character. The reason 299 | // is that the most common use case "{var}" does not capture the leading "/" 300 | // character. For consistency, all path variables must share the same behavior. 301 | // 302 | // Repeated message fields must not be mapped to URL query parameters, because 303 | // no client library can support such complicated mapping. 304 | // 305 | // If an API needs to use a JSON array for request or response body, it can map 306 | // the request or response body to a repeated field. However, some gRPC 307 | // Transcoding implementations may not support this feature. 308 | message HttpRule { 309 | // Selects a method to which this rule applies. 310 | // 311 | // Refer to [selector][google.api.DocumentationRule.selector] for syntax 312 | // details. 313 | string selector = 1; 314 | 315 | // Determines the URL pattern is matched by this rules. This pattern can be 316 | // used with any of the {get|put|post|delete|patch} methods. A custom method 317 | // can be defined using the 'custom' field. 318 | oneof pattern { 319 | // Maps to HTTP GET. Used for listing and getting information about 320 | // resources. 321 | string get = 2; 322 | 323 | // Maps to HTTP PUT. Used for replacing a resource. 324 | string put = 3; 325 | 326 | // Maps to HTTP POST. Used for creating a resource or performing an action. 327 | string post = 4; 328 | 329 | // Maps to HTTP DELETE. Used for deleting a resource. 330 | string delete = 5; 331 | 332 | // Maps to HTTP PATCH. Used for updating a resource. 333 | string patch = 6; 334 | 335 | // The custom pattern is used for specifying an HTTP method that is not 336 | // included in the `pattern` field, such as HEAD, or "*" to leave the 337 | // HTTP method unspecified for this rule. The wild-card rule is useful 338 | // for services that provide content to Web (HTML) clients. 339 | CustomHttpPattern custom = 8; 340 | } 341 | 342 | // The name of the request field whose value is mapped to the HTTP request 343 | // body, or `*` for mapping all request fields not captured by the path 344 | // pattern to the HTTP body, or omitted for not having any HTTP request body. 345 | // 346 | // NOTE: the referred field must be present at the top-level of the request 347 | // message type. 348 | string body = 7; 349 | 350 | // Optional. The name of the response field whose value is mapped to the HTTP 351 | // response body. When omitted, the entire response message will be used 352 | // as the HTTP response body. 353 | // 354 | // NOTE: The referred field must be present at the top-level of the response 355 | // message type. 356 | string response_body = 12; 357 | 358 | // Additional HTTP bindings for the selector. Nested bindings must 359 | // not contain an `additional_bindings` field themselves (that is, 360 | // the nesting may only be one level deep). 361 | repeated HttpRule additional_bindings = 11; 362 | } 363 | 364 | // A custom pattern is used for defining custom HTTP verb. 365 | message CustomHttpPattern { 366 | // The name of this custom HTTP verb. 367 | string kind = 1; 368 | 369 | // The path matched by this custom verb. 370 | string path = 2; 371 | } 372 | -------------------------------------------------------------------------------- /google/api/httpbody.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/protobuf/any.proto"; 20 | 21 | option cc_enable_arenas = true; 22 | option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "HttpBodyProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | // Message that represents an arbitrary HTTP body. It should only be used for 29 | // payload formats that can't be represented as JSON, such as raw binary or 30 | // an HTML page. 31 | // 32 | // 33 | // This message can be used both in streaming and non-streaming API methods in 34 | // the request as well as the response. 35 | // 36 | // It can be used as a top-level request field, which is convenient if one 37 | // wants to extract parameters from either the URL or HTTP template into the 38 | // request fields and also want access to the raw HTTP body. 39 | // 40 | // Example: 41 | // 42 | // message GetResourceRequest { 43 | // // A unique request id. 44 | // string request_id = 1; 45 | // 46 | // // The raw HTTP body is bound to this field. 47 | // google.api.HttpBody http_body = 2; 48 | // 49 | // } 50 | // 51 | // service ResourceService { 52 | // rpc GetResource(GetResourceRequest) 53 | // returns (google.api.HttpBody); 54 | // rpc UpdateResource(google.api.HttpBody) 55 | // returns (google.protobuf.Empty); 56 | // 57 | // } 58 | // 59 | // Example with streaming methods: 60 | // 61 | // service CaldavService { 62 | // rpc GetCalendar(stream google.api.HttpBody) 63 | // returns (stream google.api.HttpBody); 64 | // rpc UpdateCalendar(stream google.api.HttpBody) 65 | // returns (stream google.api.HttpBody); 66 | // 67 | // } 68 | // 69 | // Use of this type only changes how the request and response bodies are 70 | // handled, all other features will continue to work unchanged. 71 | message HttpBody { 72 | // The HTTP Content-Type header value specifying the content type of the body. 73 | string content_type = 1; 74 | 75 | // The HTTP request/response body as raw binary. 76 | bytes data = 2; 77 | 78 | // Application specific response metadata. Must be set in the first response 79 | // for streaming APIs. 80 | repeated google.protobuf.Any extensions = 3; 81 | } 82 | -------------------------------------------------------------------------------- /google/api/resource.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/protobuf/descriptor.proto"; 20 | 21 | option cc_enable_arenas = true; 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "ResourceProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.FieldOptions { 29 | // An annotation that describes a resource reference, see 30 | // [ResourceReference][]. 31 | google.api.ResourceReference resource_reference = 1055; 32 | } 33 | 34 | extend google.protobuf.FileOptions { 35 | // An annotation that describes a resource definition without a corresponding 36 | // message; see [ResourceDescriptor][]. 37 | repeated google.api.ResourceDescriptor resource_definition = 1053; 38 | } 39 | 40 | extend google.protobuf.MessageOptions { 41 | // An annotation that describes a resource definition, see 42 | // [ResourceDescriptor][]. 43 | google.api.ResourceDescriptor resource = 1053; 44 | } 45 | 46 | // A simple descriptor of a resource type. 47 | // 48 | // ResourceDescriptor annotates a resource message (either by means of a 49 | // protobuf annotation or use in the service config), and associates the 50 | // resource's schema, the resource type, and the pattern of the resource name. 51 | // 52 | // Example: 53 | // 54 | // message Topic { 55 | // // Indicates this message defines a resource schema. 56 | // // Declares the resource type in the format of {service}/{kind}. 57 | // // For Kubernetes resources, the format is {api group}/{kind}. 58 | // option (google.api.resource) = { 59 | // type: "pubsub.googleapis.com/Topic" 60 | // pattern: "projects/{project}/topics/{topic}" 61 | // }; 62 | // } 63 | // 64 | // The ResourceDescriptor Yaml config will look like: 65 | // 66 | // resources: 67 | // - type: "pubsub.googleapis.com/Topic" 68 | // pattern: "projects/{project}/topics/{topic}" 69 | // 70 | // Sometimes, resources have multiple patterns, typically because they can 71 | // live under multiple parents. 72 | // 73 | // Example: 74 | // 75 | // message LogEntry { 76 | // option (google.api.resource) = { 77 | // type: "logging.googleapis.com/LogEntry" 78 | // pattern: "projects/{project}/logs/{log}" 79 | // pattern: "folders/{folder}/logs/{log}" 80 | // pattern: "organizations/{organization}/logs/{log}" 81 | // pattern: "billingAccounts/{billing_account}/logs/{log}" 82 | // }; 83 | // } 84 | // 85 | // The ResourceDescriptor Yaml config will look like: 86 | // 87 | // resources: 88 | // - type: 'logging.googleapis.com/LogEntry' 89 | // pattern: "projects/{project}/logs/{log}" 90 | // pattern: "folders/{folder}/logs/{log}" 91 | // pattern: "organizations/{organization}/logs/{log}" 92 | // pattern: "billingAccounts/{billing_account}/logs/{log}" 93 | message ResourceDescriptor { 94 | // A description of the historical or future-looking state of the 95 | // resource pattern. 96 | enum History { 97 | // The "unset" value. 98 | HISTORY_UNSPECIFIED = 0; 99 | 100 | // The resource originally had one pattern and launched as such, and 101 | // additional patterns were added later. 102 | ORIGINALLY_SINGLE_PATTERN = 1; 103 | 104 | // The resource has one pattern, but the API owner expects to add more 105 | // later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents 106 | // that from being necessary once there are multiple patterns.) 107 | FUTURE_MULTI_PATTERN = 2; 108 | } 109 | 110 | // A flag representing a specific style that a resource claims to conform to. 111 | enum Style { 112 | // The unspecified value. Do not use. 113 | STYLE_UNSPECIFIED = 0; 114 | 115 | // This resource is intended to be "declarative-friendly". 116 | // 117 | // Declarative-friendly resources must be more strictly consistent, and 118 | // setting this to true communicates to tools that this resource should 119 | // adhere to declarative-friendly expectations. 120 | // 121 | // Note: This is used by the API linter (linter.aip.dev) to enable 122 | // additional checks. 123 | DECLARATIVE_FRIENDLY = 1; 124 | } 125 | 126 | // The resource type. It must be in the format of 127 | // {service_name}/{resource_type_kind}. The `resource_type_kind` must be 128 | // singular and must not include version numbers. 129 | // 130 | // Example: `storage.googleapis.com/Bucket` 131 | // 132 | // The value of the resource_type_kind must follow the regular expression 133 | // /[A-Za-z][a-zA-Z0-9]+/. It should start with an upper case character and 134 | // should use PascalCase (UpperCamelCase). The maximum number of 135 | // characters allowed for the `resource_type_kind` is 100. 136 | string type = 1; 137 | 138 | // Optional. The relative resource name pattern associated with this resource 139 | // type. The DNS prefix of the full resource name shouldn't be specified here. 140 | // 141 | // The path pattern must follow the syntax, which aligns with HTTP binding 142 | // syntax: 143 | // 144 | // Template = Segment { "/" Segment } ; 145 | // Segment = LITERAL | Variable ; 146 | // Variable = "{" LITERAL "}" ; 147 | // 148 | // Examples: 149 | // 150 | // - "projects/{project}/topics/{topic}" 151 | // - "projects/{project}/knowledgeBases/{knowledge_base}" 152 | // 153 | // The components in braces correspond to the IDs for each resource in the 154 | // hierarchy. It is expected that, if multiple patterns are provided, 155 | // the same component name (e.g. "project") refers to IDs of the same 156 | // type of resource. 157 | repeated string pattern = 2; 158 | 159 | // Optional. The field on the resource that designates the resource name 160 | // field. If omitted, this is assumed to be "name". 161 | string name_field = 3; 162 | 163 | // Optional. The historical or future-looking state of the resource pattern. 164 | // 165 | // Example: 166 | // 167 | // // The InspectTemplate message originally only supported resource 168 | // // names with organization, and project was added later. 169 | // message InspectTemplate { 170 | // option (google.api.resource) = { 171 | // type: "dlp.googleapis.com/InspectTemplate" 172 | // pattern: 173 | // "organizations/{organization}/inspectTemplates/{inspect_template}" 174 | // pattern: "projects/{project}/inspectTemplates/{inspect_template}" 175 | // history: ORIGINALLY_SINGLE_PATTERN 176 | // }; 177 | // } 178 | History history = 4; 179 | 180 | // The plural name used in the resource name and permission names, such as 181 | // 'projects' for the resource name of 'projects/{project}' and the permission 182 | // name of 'cloudresourcemanager.googleapis.com/projects.get'. One exception 183 | // to this is for Nested Collections that have stuttering names, as defined 184 | // in [AIP-122](https://google.aip.dev/122#nested-collections), where the 185 | // collection ID in the resource name pattern does not necessarily directly 186 | // match the `plural` value. 187 | // 188 | // It is the same concept of the `plural` field in k8s CRD spec 189 | // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ 190 | // 191 | // Note: The plural form is required even for singleton resources. See 192 | // https://aip.dev/156 193 | string plural = 5; 194 | 195 | // The same concept of the `singular` field in k8s CRD spec 196 | // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ 197 | // Such as "project" for the `resourcemanager.googleapis.com/Project` type. 198 | string singular = 6; 199 | 200 | // Style flag(s) for this resource. 201 | // These indicate that a resource is expected to conform to a given 202 | // style. See the specific style flags for additional information. 203 | repeated Style style = 10; 204 | } 205 | 206 | // Defines a proto annotation that describes a string field that refers to 207 | // an API resource. 208 | message ResourceReference { 209 | // The resource type that the annotated field references. 210 | // 211 | // Example: 212 | // 213 | // message Subscription { 214 | // string topic = 2 [(google.api.resource_reference) = { 215 | // type: "pubsub.googleapis.com/Topic" 216 | // }]; 217 | // } 218 | // 219 | // Occasionally, a field may reference an arbitrary resource. In this case, 220 | // APIs use the special value * in their resource reference. 221 | // 222 | // Example: 223 | // 224 | // message GetIamPolicyRequest { 225 | // string resource = 2 [(google.api.resource_reference) = { 226 | // type: "*" 227 | // }]; 228 | // } 229 | string type = 1; 230 | 231 | // The resource type of a child collection that the annotated field 232 | // references. This is useful for annotating the `parent` field that 233 | // doesn't have a fixed resource type. 234 | // 235 | // Example: 236 | // 237 | // message ListLogEntriesRequest { 238 | // string parent = 1 [(google.api.resource_reference) = { 239 | // child_type: "logging.googleapis.com/LogEntry" 240 | // }; 241 | // } 242 | string child_type = 2; 243 | } 244 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if a command exists 4 | command_exists() { 5 | command -v "$1" >/dev/null 2>&1 6 | } 7 | 8 | # Check for required dependencies 9 | dependencies=( "go" "git" "docker" "minikube" "kubectl" "sudo" ) 10 | missing=() 11 | 12 | for dep in "${dependencies[@]}"; do 13 | if ! command_exists "$dep"; then 14 | missing+=("$dep") 15 | fi 16 | done 17 | 18 | if [ ${#missing[@]} -ne 0 ]; then 19 | echo "❌ The following dependencies are missing: ${missing[*]}" 20 | echo "Please install them before running this script." 21 | exit 1 22 | fi 23 | 24 | # Build the thunder-generate binary 25 | echo "⚙️ Building thunder-generate..." 26 | go build -o ./cmd/protoc-gen-rpc-impl ./cmd/protoc-gen-rpc-impl.go 27 | sudo mv ./cmd/protoc-gen-rpc-impl /usr/local/bin 28 | sudo chmod +x /usr/local/bin/protoc-gen-rpc-impl 29 | go build -o thunder-generate generator.go 30 | 31 | # Move thunder-generate to /usr/local/bin/ 32 | echo "🚀 Moving thunder-generate to /usr/local/bin/..." 33 | sudo mv thunder-generate /usr/local/bin/ 34 | 35 | # Create the thunder command script 36 | echo "🛠️ Creating thunder command script..." 37 | cat << 'EOF' | sudo tee /usr/local/bin/thunder > /dev/null 38 | #!/bin/bash 39 | 40 | case "$1" in 41 | init) 42 | shift 43 | # Use an optional directory name; default to "Thunder" 44 | TARGET_DIR="${1:-Thunder}" 45 | # Remove or comment out the logging below if you don't want it printed: 46 | # echo "Cloning Thunder repository into '${TARGET_DIR}'..." 47 | git clone -q https://github.com/Raezil/Thunder "$TARGET_DIR" || { echo "❌ Error: Cloning failed."; exit 1; } 48 | rm -rf "$TARGET_DIR/.git" || { echo "❌ Error: Could not remove .git folder."; exit 1; } 49 | echo "Creating new Thunder project: '${TARGET_DIR}'." 50 | echo "Setting up project structure..." 51 | echo -e "\e[32m✓ Project created successfully!\e[0m" 52 | ;; 53 | generate) 54 | shift 55 | thunder-generate "$@" 56 | ;; 57 | build) 58 | DEPLOYMENT_FILE="./k8s/app-deployment.yaml" 59 | 60 | # Verify the deployment file exists. 61 | if [ ! -f "$DEPLOYMENT_FILE" ]; then 62 | echo "Error: $DEPLOYMENT_FILE not found." 63 | exit 1 64 | fi 65 | echo "Enter your Docker Hub username:" 66 | read docker_username 67 | echo "Enter your Docker project name:" 68 | read docker_project 69 | echo "🔨 Building Docker image..." 70 | NEW_IMAGE="${docker_username}/${docker_project}:latest" 71 | sed -i'' -E '/busybox/! s#^([[:space:]]*image:[[:space:]])[^[:space:]]+#\1'"${NEW_IMAGE}"'#' k8s/app-deployment.yaml 72 | docker build -t ${docker_username}/${docker_project}:latest . 73 | echo "🔑 Logging in to Docker Hub..." 74 | docker login 75 | echo "⬆️ Pushing Docker image..." 76 | docker push ${docker_username}/${docker_project}:latest 77 | echo -e "\e[32m✓ Successfully built ${NEW_IMAGE}!\e[0m" 78 | ;; 79 | deploy) 80 | echo "🚀 Starting Minikube..." 81 | minikube start 82 | 83 | # Change to Kubernetes manifests directory 84 | cd k8s || { echo "❌ Directory k8s not found!"; exit 1; } 85 | 86 | # Apply PostgreSQL resources 87 | echo "📦 Deploying PostgreSQL..." 88 | kubectl apply -f postgres-deployment.yaml 89 | kubectl apply -f postgres-service.yaml 90 | kubectl apply -f postgres-pvc.yaml 91 | 92 | # Wait for PostgreSQL to be ready 93 | echo "⏳ Waiting for PostgreSQL to be ready..." 94 | kubectl wait --for=condition=ready pod -l app=postgres --timeout=60s 95 | 96 | # Apply application deployments and services 97 | echo "⚙️ Deploying Thunder API..." 98 | kubectl apply -f app-deployment.yaml 99 | kubectl apply -f app-service.yaml 100 | kubectl apply -f app-loadbalancer.yaml 101 | # Apply HPA configuration 102 | kubectl apply -f hpa.yaml 103 | kubectl apply -f network-policy.yaml 104 | # Apply PgBouncer for database connection pooling 105 | echo "🔄 Deploying PgBouncer..." 106 | kubectl apply -f pgbouncer-all.yaml 107 | 108 | # Restart necessary deployments 109 | echo "🔄 Restarting PgBouncer and Thunder API deployments..." 110 | kubectl rollout restart deployment pgbouncer 111 | kubectl rollout restart deployment app-deployment 112 | echo -e "\e[32m ✓ Deployment successful!!\e[0m" 113 | 114 | # Port forward the app service 115 | echo "🔗 Forwarding port 8080 to app-service..." 116 | kubectl port-forward service/app-service 8080:8080 & 117 | ;; 118 | test) 119 | echo "Running tests..." 120 | go test -v ./pkg/db ./pkg/middlewares/ ./pkg/services/ ./pkg/services/generated 121 | exit 0 122 | ;; 123 | serve) 124 | cd ./cmd/app/server 125 | go run main.go 126 | exit 0 127 | ;; 128 | *) 129 | echo "⚡ Usage: $0 [init | docker | generate | deploy | test]" 130 | exit 1 131 | ;; 132 | esac 133 | EOF 134 | 135 | # Make thunder executable 136 | echo "🔧 Making thunder command executable..." 137 | sudo chmod +x /usr/local/bin/thunder 138 | 139 | echo "✅ Installation complete! You can now use 'thunder init', 'thunder build', 'thunder generate' and 'thunder deploy'." 140 | -------------------------------------------------------------------------------- /k8s/app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: app-deployment 5 | labels: 6 | app: app 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: app 12 | template: 13 | metadata: 14 | labels: 15 | app: app 16 | spec: 17 | initContainers: 18 | - name: wait-for-postgres 19 | image: busybox:1.28 20 | command: ['sh', '-c', 'until nc -z postgres-service 5432; do echo "waiting for postgres"; sleep 2; done'] 21 | containers: 22 | - name: app 23 | image: raezil/app:latest 24 | imagePullPolicy: Always 25 | ports: 26 | - containerPort: 50051 27 | - containerPort: 8080 28 | env: 29 | - name: PGHOST 30 | value: "postgres-service" 31 | - name: POSTGRES_USER 32 | valueFrom: 33 | secretKeyRef: 34 | name: postgres-secret 35 | key: POSTGRES_USER 36 | - name: POSTGRES_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: postgres-secret 40 | key: POSTGRES_PASSWORD 41 | - name: POSTGRES_DB 42 | valueFrom: 43 | secretKeyRef: 44 | name: postgres-secret 45 | key: POSTGRES_DB 46 | - name: DATABASE_URL 47 | valueFrom: 48 | secretKeyRef: 49 | name: app-secret 50 | key: DATABASE_URL 51 | - name: JWT_SECRET 52 | valueFrom: 53 | secretKeyRef: 54 | name: app-secret 55 | key: JWT_SECRET -------------------------------------------------------------------------------- /k8s/app-loadbalancer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: app-loadbalancer 5 | spec: 6 | type: LoadBalancer 7 | selector: 8 | app: app 9 | ports: 10 | - name: grpc 11 | protocol: TCP 12 | port: 50051 13 | targetPort: 50051 14 | - name: http 15 | protocol: TCP 16 | port: 8080 17 | targetPort: 8080 18 | -------------------------------------------------------------------------------- /k8s/app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: app-service 5 | labels: 6 | app: app 7 | spec: 8 | selector: 9 | app: app 10 | ports: 11 | - name: grpc 12 | port: 50051 13 | targetPort: 50051 14 | protocol: TCP 15 | - name: http 16 | port: 8080 17 | targetPort: 8080 18 | protocol: TCP 19 | type: ClusterIP 20 | -------------------------------------------------------------------------------- /k8s/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: thunder-hpa 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: thunder-deployment 10 | minReplicas: 2 11 | maxReplicas: 10 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 70 19 | -------------------------------------------------------------------------------- /k8s/network-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: restrict-app-traffic 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app: app 9 | policyTypes: 10 | - Ingress 11 | - Egress 12 | ingress: 13 | - from: 14 | - podSelector: 15 | matchLabels: 16 | app: postgres 17 | ports: 18 | - protocol: TCP 19 | port: 5432 20 | egress: 21 | - to: 22 | - podSelector: 23 | matchLabels: 24 | app: postgres 25 | ports: 26 | - protocol: TCP 27 | port: 5432 28 | -------------------------------------------------------------------------------- /k8s/pgbouncer-all.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: pgbouncer-config 6 | data: 7 | pgbouncer.ini: | 8 | [databases] 9 | thunder = host=postgres-service port=5432 dbname=thunder 10 | 11 | [pgbouncer] 12 | listen_addr = 0.0.0.0 13 | listen_port = 6432 14 | auth_type = md5 15 | auth_file = /etc/pgbouncer/userlist.txt 16 | pool_mode = session 17 | max_client_conn = 100 18 | default_pool_size = 20 19 | --- 20 | apiVersion: v1 21 | kind: Secret 22 | metadata: 23 | name: pgbouncer-secret 24 | type: Opaque 25 | data: 26 | userlist.txt: InBvc3RncmVzIiAicG9zdGdyZXMi 27 | --- 28 | apiVersion: apps/v1 29 | kind: Deployment 30 | metadata: 31 | name: pgbouncer 32 | spec: 33 | replicas: 1 34 | selector: 35 | matchLabels: 36 | app: pgbouncer 37 | template: 38 | metadata: 39 | labels: 40 | app: pgbouncer 41 | spec: 42 | containers: 43 | - name: pgbouncer 44 | image: edoburu/pgbouncer:latest 45 | ports: 46 | - containerPort: 6432 47 | volumeMounts: 48 | - name: config-volume 49 | mountPath: /etc/pgbouncer 50 | volumes: 51 | - name: config-volume 52 | projected: 53 | sources: 54 | - configMap: 55 | name: pgbouncer-config 56 | items: 57 | - key: pgbouncer.ini 58 | path: pgbouncer.ini 59 | - secret: 60 | name: pgbouncer-secret 61 | items: 62 | - key: userlist.txt 63 | path: userlist.txt 64 | --- 65 | apiVersion: v1 66 | kind: Service 67 | metadata: 68 | name: pgbouncer-service 69 | spec: 70 | selector: 71 | app: pgbouncer 72 | ports: 73 | - name: pgbouncer 74 | port: 6432 75 | targetPort: 6432 76 | type: ClusterIP 77 | -------------------------------------------------------------------------------- /k8s/postgres-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: postgres-deployment 5 | labels: 6 | app: postgres 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: postgres 12 | template: 13 | metadata: 14 | labels: 15 | app: postgres 16 | spec: 17 | containers: 18 | - name: postgres 19 | image: postgres:15 20 | ports: 21 | - containerPort: 5432 22 | env: 23 | - name: POSTGRES_USER 24 | valueFrom: 25 | secretKeyRef: 26 | name: postgres-secret 27 | key: POSTGRES_USER 28 | - name: POSTGRES_PASSWORD 29 | valueFrom: 30 | secretKeyRef: 31 | name: postgres-secret 32 | key: POSTGRES_PASSWORD 33 | - name: POSTGRES_DB 34 | valueFrom: 35 | secretKeyRef: 36 | name: postgres-secret 37 | key: POSTGRES_DB 38 | volumeMounts: 39 | - name: postgres-data 40 | mountPath: /var/lib/postgresql/data 41 | volumes: 42 | - name: postgres-data 43 | persistentVolumeClaim: 44 | claimName: postgres-pvc 45 | -------------------------------------------------------------------------------- /k8s/postgres-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: postgres-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 1Gi 11 | -------------------------------------------------------------------------------- /k8s/postgres-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres-service 5 | spec: 6 | selector: 7 | app: postgres 8 | ports: 9 | - port: 5432 10 | targetPort: 5432 11 | protocol: TCP 12 | type: ClusterIP 13 | -------------------------------------------------------------------------------- /pkg/db/.gitignore: -------------------------------------------------------------------------------- 1 | # gitignore generated by Prisma Client Go. DO NOT EDIT. 2 | *_gen.go 3 | -------------------------------------------------------------------------------- /pkg/db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | // main.go 11 | func GetUserName(ctx context.Context, client *PrismaClient, postID string) (string, error) { 12 | user, err := client.User.FindUnique( 13 | User.ID.Equals(postID), 14 | ).Exec(ctx) 15 | if err != nil { 16 | return "", fmt.Errorf("error fetching user: %w", err) 17 | } 18 | 19 | return user.Name, nil 20 | } 21 | 22 | func TestGetUserName_error(t *testing.T) { 23 | client, mock, ensure := NewMock() 24 | defer ensure(t) 25 | 26 | mock.User.Expect( 27 | client.User.FindUnique( 28 | User.ID.Equals("123"), 29 | ), 30 | ).Errors(ErrNotFound) 31 | 32 | _, err := GetUserName(context.Background(), client, "123") 33 | if !errors.Is(err, ErrNotFound) { 34 | t.Fatalf("error expected to return ErrNotFound but is %s", err) 35 | } 36 | } 37 | 38 | func TestGetUserName_returns(t *testing.T) { 39 | // create a new mock 40 | // this returns a mock prisma `client` and a `mock` object to set expectations 41 | client, mock, ensure := NewMock() 42 | // defer calling ensure, which makes sure all of the expectations were met and actually called 43 | // calling this makes sure that an error is returned if there was no query happening for a given expectation 44 | // and makes sure that all of them succeeded 45 | defer ensure(t) 46 | 47 | expected := UserModel{ 48 | InnerUser: InnerUser{ 49 | ID: "123", 50 | Name: "foo", 51 | Email: "kmosc@protonmail.com", 52 | Password: "password", 53 | }, 54 | } 55 | 56 | // start the expectation 57 | mock.User.Expect( 58 | // define your exact query as in your tested function 59 | // call it with the exact arguments which you expect the function to be called with 60 | // you can copy and paste this from your tested function, and just put specific values into the arguments 61 | client.User.FindUnique( 62 | User.ID.Equals("123"), 63 | ), 64 | ).Returns(expected) // sets the object which should be returned in the function call 65 | 66 | // mocking set up is done; let's define the actual test now 67 | name, err := GetUserName(context.Background(), client, "123") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | if name != "foo" { 73 | t.Fatalf("name expected to be foo but is %s", name) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/db/go.mod: -------------------------------------------------------------------------------- 1 | module db 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/shopspring/decimal v1.4.0 8 | github.com/steebchen/prisma-client-go v0.47.0 9 | ) 10 | 11 | require go.mongodb.org/mongo-driver/v2 v2.0.1 // indirect 12 | -------------------------------------------------------------------------------- /pkg/db/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 6 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 10 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 11 | github.com/steebchen/prisma-client-go v0.47.0 h1:mKelgkcGPcIardjTP5diGq6hvnueQc/DYEyQ+6uZ0/E= 12 | github.com/steebchen/prisma-client-go v0.47.0/go.mod h1:i1B0PEaE+BUcBUiwvd9drWpyMG/zNYMRrD5MancMf2I= 13 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 14 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | go.mongodb.org/mongo-driver/v2 v2.0.1 h1:mhB/ZJkLSv6W6LGzY7sEjpZif47+JdfEEXjlLCIv7Qc= 16 | go.mongodb.org/mongo-driver/v2 v2.0.1/go.mod h1:w7iFnTcQDMXtdXwcvyG3xljYpoBa1ErkI0yOzbkZ9b8= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /pkg/middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | pb "services" 7 | 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/metadata" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | // middleware verifies JWT tokens in the request context. 15 | // Rejects unauthorized requests with a detailed log entry. 16 | func AuthUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 17 | if info.FullMethod == "/authenticator.Auth/Login" || info.FullMethod == "/authenticator.Auth/Register" { 18 | return handler(ctx, req) 19 | } 20 | md, ok := metadata.FromIncomingContext(ctx) 21 | if !ok { 22 | return nil, fmt.Errorf("missing metadata") 23 | } 24 | token := md["authorization"] 25 | if len(token) == 0 { 26 | return nil, status.Errorf(codes.Unauthenticated, "missing token") 27 | } 28 | 29 | claims, err := pb.VerifyJWT(token[0]) 30 | if err != nil { 31 | return nil, status.Errorf(codes.Unauthenticated, "unauthorized: %v", err) 32 | } 33 | // Set timeout for database operations to prevent hanging requests 34 | md = metadata.Join(md, metadata.Pairs("current_user", claims.Email)) 35 | ctx = metadata.NewIncomingContext(ctx, md) 36 | return handler(ctx, req) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/middlewares/chain_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | // ChainUnaryInterceptors manually chains multiple gRPC Unary Interceptors 10 | func ChainUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { 11 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 12 | // Recursive call to chain interceptors 13 | var chainHandler grpc.UnaryHandler 14 | chainHandler = handler 15 | 16 | // Apply interceptors in reverse order (last one runs first) 17 | for i := len(interceptors) - 1; i >= 0; i-- { 18 | interceptor := interceptors[i] 19 | next := chainHandler 20 | chainHandler = func(c context.Context, r interface{}) (interface{}, error) { 21 | return interceptor(c, r, info, next) 22 | } 23 | } 24 | 25 | // Call the first interceptor 26 | return chainHandler(ctx, req) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/middlewares/cors.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import "github.com/valyala/fasthttp" 4 | 5 | // CORSMiddleware adds CORS headers to fasthttp requests. 6 | func CORSMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler { 7 | return func(ctx *fasthttp.RequestCtx) { 8 | // Set CORS headers on the response. 9 | ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") 10 | ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 11 | ctx.Response.Header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 12 | 13 | // Handle preflight request. 14 | if string(ctx.Method()) == "OPTIONS" { 15 | ctx.SetStatusCode(fasthttp.StatusNoContent) 16 | return 17 | } 18 | 19 | // Continue processing the request. 20 | next(ctx) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/middlewares/go.mod: -------------------------------------------------------------------------------- 1 | module middlewares 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/valyala/fasthttp v1.59.0 7 | golang.org/x/time v0.10.0 8 | google.golang.org/grpc v1.71.0 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/brotli v1.1.1 // indirect 13 | github.com/klauspost/compress v1.17.11 // indirect 14 | github.com/valyala/bytebufferpool v1.0.0 // indirect 15 | golang.org/x/net v0.35.0 // indirect 16 | golang.org/x/sys v0.30.0 // indirect 17 | golang.org/x/text v0.22.0 // indirect 18 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 19 | google.golang.org/protobuf v1.36.4 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /pkg/middlewares/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 6 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 7 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 8 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 14 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 15 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 16 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 17 | github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= 18 | github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= 19 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 20 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 21 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 22 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 23 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 24 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 25 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 26 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 27 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 28 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 29 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 30 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 31 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 32 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 33 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 34 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 35 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 36 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 37 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 38 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 39 | golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= 40 | golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 41 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 42 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 43 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 44 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 45 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 46 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 47 | -------------------------------------------------------------------------------- /pkg/middlewares/logging.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | // LoggingMiddleware logs request method, path, status code, and duration. 11 | func LoggingMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler { 12 | return func(ctx *fasthttp.RequestCtx) { 13 | start := time.Now() 14 | 15 | next(ctx) // Execute the next handler 16 | 17 | duration := time.Since(start) 18 | 19 | log.Printf("[HTTP] %s %s %d %s", 20 | string(ctx.Method()), ctx.Path(), ctx.Response.StatusCode(), duration) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/middlewares/middlewares_test.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | // Test CORS Middleware with a simulated OPTIONS request 11 | func TestCORSMiddleware(t *testing.T) { 12 | mockHandler := func(ctx *fasthttp.RequestCtx) { 13 | ctx.SetStatusCode(200) 14 | } 15 | 16 | cors := CORSMiddleware(mockHandler) 17 | 18 | // Simulate an OPTIONS request 19 | ctx := &fasthttp.RequestCtx{} 20 | ctx.Request.SetRequestURI("/test") 21 | ctx.Request.Header.SetMethod("OPTIONS") 22 | 23 | cors(ctx) 24 | 25 | // Debug headers 26 | t.Logf("Response Headers:\n%s", ctx.Response.Header.String()) 27 | 28 | // Expected CORS headers 29 | expectedHeaders := map[string]string{ 30 | "Access-Control-Allow-Origin": "*", 31 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 32 | "Access-Control-Allow-Headers": "Content-Type, Authorization", 33 | } 34 | 35 | // Check if headers are correctly set 36 | for key, expectedValue := range expectedHeaders { 37 | value := string(ctx.Response.Header.Peek(key)) 38 | if value != expectedValue { 39 | t.Errorf("Expected %s: %s, but got %s", key, expectedValue, value) 40 | } 41 | } 42 | 43 | // Ensure correct status code for preflight requests 44 | if ctx.Response.StatusCode() != fasthttp.StatusNoContent { 45 | t.Errorf("Expected status 204, got %d", ctx.Response.StatusCode()) 46 | } 47 | } 48 | 49 | // Test Rate Limiting Middleware 50 | func TestRateLimiter(t *testing.T) { 51 | limiter := NewRateLimiter(1, 1) // 1 request per second 52 | 53 | clientID := "test-client" 54 | 55 | // First request should pass 56 | if !limiter.GetLimiter(clientID).Allow() { 57 | t.Error("Expected first request to pass") 58 | } 59 | 60 | // Second request should be rate-limited 61 | if limiter.GetLimiter(clientID).Allow() { 62 | t.Error("Expected second request to be blocked") 63 | } 64 | 65 | // Wait for rate limit to reset 66 | time.Sleep(time.Second) 67 | 68 | if !limiter.GetLimiter(clientID).Allow() { 69 | t.Error("Expected request after reset to pass") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/middlewares/rate_limiter.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "golang.org/x/time/rate" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/peer" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | // RateLimiter structure 16 | type RateLimiter struct { 17 | mu sync.Mutex 18 | limiters map[string]*rate.Limiter 19 | rate rate.Limit 20 | burst int 21 | } 22 | 23 | // NewRateLimiter initializes a rate limiter 24 | func NewRateLimiter(r rate.Limit, b int) *RateLimiter { 25 | return &RateLimiter{ 26 | limiters: make(map[string]*rate.Limiter), 27 | rate: r, 28 | burst: b, 29 | } 30 | } 31 | 32 | // getLimiter gets or creates a rate limiter for a specific client 33 | func (r *RateLimiter) GetLimiter(clientID string) *rate.Limiter { 34 | r.mu.Lock() 35 | defer r.mu.Unlock() 36 | 37 | if limiter, exists := r.limiters[clientID]; exists { 38 | return limiter 39 | } 40 | 41 | limiter := rate.NewLimiter(r.rate, r.burst) 42 | r.limiters[clientID] = limiter 43 | 44 | // Cleanup old limiters after a timeout 45 | go func() { 46 | time.Sleep(10 * time.Minute) 47 | r.mu.Lock() 48 | delete(r.limiters, clientID) 49 | r.mu.Unlock() 50 | }() 51 | 52 | return limiter 53 | } 54 | 55 | // RateLimiterInterceptor applies rate limiting 56 | func (r *RateLimiter) RateLimiterInterceptor( 57 | ctx context.Context, 58 | req interface{}, 59 | info *grpc.UnaryServerInfo, 60 | handler grpc.UnaryHandler) (interface{}, error) { 61 | 62 | // Extract client identifier (can use IP or API key) 63 | // Extract the client's IP address from the context using the peer package. 64 | p, ok := peer.FromContext(ctx) 65 | var clientID string 66 | if ok { 67 | clientID = p.Addr.String() 68 | // Optionally, further parse clientID if needed (e.g., remove port information) 69 | } else { 70 | clientID = "unknown" 71 | } 72 | limiter := r.GetLimiter(clientID) 73 | if !limiter.Allow() { 74 | return nil, status.Errorf(codes.ResourceExhausted, "Too many requests, slow down") 75 | } 76 | 77 | // Proceed to the next handler 78 | return handler(ctx, req) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/routes/generated_register.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "context" 5 | "log" 6 | . "generated" 7 | 8 | "google.golang.org/grpc" 9 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 10 | "go.uber.org/zap" 11 | "db" 12 | pb "services" 13 | ) 14 | 15 | // RegisterServers registers gRPC services to the server. 16 | func RegisterServers(server *grpc.Server, client *db.PrismaClient, sugar *zap.SugaredLogger) { 17 | 18 | RegisterAuthServer(server, &pb.AuthServiceServer{ 19 | PrismaClient: client, 20 | Logger: sugar, 21 | }) 22 | 23 | } 24 | 25 | // RegisterHandlers registers gRPC-Gateway handlers. 26 | func RegisterHandlers(gwmux *runtime.ServeMux, conn *grpc.ClientConn) { 27 | var err error 28 | 29 | err = RegisterAuthHandler(context.Background(), gwmux, conn) 30 | if err != nil { 31 | log.Fatalln("Failed to register gateway:", err) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /pkg/routes/go.mod: -------------------------------------------------------------------------------- 1 | module routes 2 | 3 | go 1.22.2 4 | -------------------------------------------------------------------------------- /pkg/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator db { 7 | provider = "go run github.com/steebchen/prisma-client-go" 8 | } 9 | 10 | model User { 11 | id String @default(cuid()) @id 12 | createdAt DateTime @default(now()) 13 | updatedAt DateTime @updatedAt 14 | name String 15 | password String 16 | email String @unique 17 | Age Int 18 | desc String? 19 | } -------------------------------------------------------------------------------- /pkg/services/auth_integration_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "github.com/testcontainers/testcontainers-go" 17 | "github.com/testcontainers/testcontainers-go/wait" 18 | ) 19 | 20 | // User represents the payload structure for both registration and login. 21 | type User struct { 22 | Email string `json:"email"` 23 | Password string `json:"password"` 24 | Name string `json:"name,omitempty"` 25 | Surname string `json:"surname,omitempty"` 26 | Age int `json:"age,omitempty"` 27 | } 28 | 29 | type TokenResponse struct { 30 | Token string `json:"token"` 31 | } 32 | 33 | func TestContainers(t *testing.T) { 34 | ctx := context.Background() 35 | networkName := fmt.Sprintf("test-network-%d", time.Now().UnixNano()) 36 | networkReq := testcontainers.NetworkRequest{ 37 | Name: networkName, 38 | CheckDuplicate: true, 39 | } 40 | network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ 41 | NetworkRequest: networkReq, 42 | }) 43 | if err != nil { 44 | t.Fatalf("failed to create network: %v", err) 45 | } 46 | defer network.Remove(ctx) 47 | 48 | // Launch PostgreSQL container with a network alias. 49 | postgresReq := testcontainers.ContainerRequest{ 50 | Image: "postgres:13", 51 | ExposedPorts: []string{"5432/tcp"}, 52 | Env: map[string]string{ 53 | "POSTGRES_USER": "postgres", 54 | "POSTGRES_PASSWORD": "postgres", 55 | "POSTGRES_DB": "testdb", 56 | }, 57 | Networks: []string{networkName}, 58 | // Set the network alias so that other containers can refer to it by name. 59 | NetworkAliases: map[string][]string{ 60 | networkName: {"postgres"}, 61 | }, 62 | WaitingFor: wait.ForListeningPort("5432/tcp"), 63 | } 64 | postgresC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 65 | ContainerRequest: postgresReq, 66 | Started: true, 67 | }) 68 | if err != nil { 69 | t.Fatalf("failed to start postgres container: %v", err) 70 | } 71 | defer postgresC.Terminate(ctx) 72 | 73 | // Update the connection string to use the network alias "postgres". 74 | dbConnStr := "postgres://postgres:postgres@postgres:5432/testdb?sslmode=disable" 75 | t.Logf("Postgres connection string: %s", dbConnStr) 76 | 77 | // Launch the application container on the same network. 78 | appReq := testcontainers.ContainerRequest{ 79 | Image: "raezil/app:latest", 80 | ExposedPorts: []string{"8080/tcp"}, 81 | Env: map[string]string{ 82 | "DATABASE_URL": dbConnStr, 83 | "JWT_SECRET": "supersecret", 84 | }, 85 | Networks: []string{networkName}, 86 | WaitingFor: wait.ForListeningPort("8080/tcp"), 87 | } 88 | appContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 89 | ContainerRequest: appReq, 90 | Started: true, 91 | }) 92 | if err != nil { 93 | // Retrieve and log container logs for debugging. 94 | if appContainer != nil { 95 | logs, logErr := appContainer.Logs(ctx) 96 | if logErr == nil { 97 | buf := new(strings.Builder) 98 | _, copyErr := io.Copy(buf, logs) 99 | if copyErr == nil { 100 | t.Logf("Application container logs:\n%s", buf.String()) 101 | } else { 102 | t.Logf("Failed to read container logs: %v", copyErr) 103 | } 104 | } else { 105 | t.Logf("Failed to get container logs: %v", logErr) 106 | } 107 | } 108 | t.Fatalf("failed to start raezil/app:latest container: %v", err) 109 | } 110 | defer appContainer.Terminate(ctx) 111 | 112 | // Retrieve host and port for the application container. 113 | appHost, err := appContainer.Host(ctx) 114 | if err != nil { 115 | t.Fatalf("failed to get app container host: %v", err) 116 | } 117 | mappedPort, err := appContainer.MappedPort(ctx, "8080/tcp") 118 | if err != nil { 119 | t.Fatalf("failed to get mapped port: %v", err) 120 | } 121 | appURL := fmt.Sprintf("https://%s:%s", appHost, mappedPort.Port()) 122 | 123 | t.Logf("Application is running at %s", appURL) 124 | // Optionally wait for a few seconds to ensure the application is fully started. 125 | time.Sleep(45 * time.Second) 126 | 127 | // Configure an HTTP client. If your app doesn't use TLS, change the scheme above to "http". 128 | client := &http.Client{ 129 | Transport: &http.Transport{ 130 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 131 | }, 132 | } 133 | 134 | // Base URLs and payloads. 135 | registerURL := appURL + "/v1/auth/register" 136 | loginURL := appURL + "/v1/auth/login" 137 | protectedURL := appURL + "/v1/auth/protected" 138 | registerPayload := User{ 139 | Email: "newuser@example.com", 140 | Password: "password123", 141 | Name: "John", 142 | Surname: "Doe", 143 | Age: 30, 144 | } 145 | loginPayload := User{ 146 | Email: "newuser@example.com", 147 | Password: "password123", 148 | } 149 | 150 | // Registration: simulate 151 | if err := postJSON(client, registerURL, registerPayload, 200); err != nil { 152 | t.Fatalf("registration failed: %v", err) 153 | } 154 | 155 | // Login: simulate to verify basic functionality. 156 | if err := postJSON(client, loginURL, loginPayload, 20); err != nil { 157 | t.Fatalf("login failed: %v", err) 158 | } 159 | 160 | var tokenResp TokenResponse 161 | if err := postJSONWithResponse(client, loginURL, loginPayload, 200, &tokenResp); err != nil { 162 | t.Fatalf("login failed: %v", err) 163 | } 164 | 165 | if err := getJSONWithAuth(client, protectedURL+"?text=hello", 200, tokenResp.Token); err != nil { 166 | t.Fatalf("protected request failed: %v", err) 167 | } 168 | t.Log("Both registration and login returned the expected status codes.") 169 | 170 | // ------------------------- 171 | // Additional Sub-Tests 172 | // ------------------------- 173 | 174 | // Test duplicate registration with the same email. 175 | t.Run("Duplicate Registration", func(t *testing.T) { 176 | // Attempt to register the same user again; expecting a failure (e.g. 400 Bad Request). 177 | err := postJSON(client, registerURL, registerPayload, 40) // 40 becomes 400. 178 | if err != nil { 179 | t.Logf("Duplicate registration failed as expected: %v", err) 180 | } else { 181 | t.Error("Duplicate registration succeeded unexpectedly") 182 | } 183 | }) 184 | 185 | // Test login with an incorrect password. 186 | t.Run("Login with Wrong Password", func(t *testing.T) { 187 | wrongLogin := User{ 188 | Email: "newuser@example.com", 189 | Password: "wrongpassword", 190 | } 191 | // Expecting an unauthorized response (e.g. 401 Unauthorized). 192 | err := postJSON(client, loginURL, wrongLogin, 40) // 40 becomes 400 or 401, adjust as needed. 193 | if err != nil { 194 | t.Logf("Login with wrong password failed as expected: %v", err) 195 | } else { 196 | t.Error("Login with wrong password succeeded unexpectedly") 197 | } 198 | }) 199 | 200 | // Test accessing the protected endpoint without a token. 201 | t.Run("Protected Endpoint Without Token", func(t *testing.T) { 202 | // No Authorization header is set. 203 | 204 | err := getJSON(client, protectedURL+"?text=hello", 40) // expecting failure (e.g. 401 Unauthorized). 205 | if err != nil { 206 | t.Logf("Protected endpoint access without token failed as expected: %v", err) 207 | } else { 208 | t.Error("Access to protected endpoint without token succeeded unexpectedly") 209 | } 210 | }) 211 | 212 | // Test accessing the protected endpoint with an invalid token. 213 | t.Run("Protected Endpoint With Invalid Token", func(t *testing.T) { 214 | invalidToken := "invalid-token" 215 | // Expecting failure (e.g. 401 Unauthorized). 216 | err := getJSONWithAuth(client, protectedURL+"?text=hello", 40, invalidToken) 217 | if err != nil { 218 | t.Logf("Protected endpoint access with invalid token failed as expected: %v", err) 219 | } else { 220 | t.Error("Access to protected endpoint with invalid token succeeded unexpectedly") 221 | } 222 | }) 223 | } 224 | 225 | // postJSONWithResponse simulates a curl POST request with JSON payload, 226 | // validates the HTTP response status code, and decodes the JSON response. 227 | func postJSONWithResponse(client *http.Client, url string, data interface{}, expectedStatus int, response interface{}) error { 228 | payloadBytes, err := json.Marshal(data) 229 | if err != nil { 230 | return fmt.Errorf("failed to marshal JSON: %w", err) 231 | } 232 | 233 | req, err := http.NewRequest("POST", url, strings.NewReader(string(payloadBytes))) 234 | if err != nil { 235 | return fmt.Errorf("failed to create request: %w", err) 236 | } 237 | req.Header.Set("Content-Type", "application/json") 238 | 239 | resp, err := client.Do(req) 240 | if err != nil { 241 | return fmt.Errorf("request failed: %w", err) 242 | } 243 | defer resp.Body.Close() 244 | 245 | if resp.StatusCode != expectedStatus { 246 | return fmt.Errorf("unexpected status code: got %d, expected %d", resp.StatusCode, expectedStatus) 247 | } 248 | 249 | return json.NewDecoder(resp.Body).Decode(response) 250 | } 251 | 252 | // postJSON simulates a curl POST request with JSON payload, 253 | // validates the HTTP response status code, and logs the response. 254 | func postJSON(client *http.Client, url string, data interface{}, expectedStatus int) error { 255 | payloadBytes, err := json.Marshal(data) 256 | if err != nil { 257 | return fmt.Errorf("failed to marshal JSON: %w", err) 258 | } 259 | 260 | req, err := http.NewRequest("POST", url, strings.NewReader(string(payloadBytes))) 261 | if err != nil { 262 | return fmt.Errorf("failed to create request: %w", err) 263 | } 264 | req.Header.Set("Content-Type", "application/json") 265 | 266 | resp, err := client.Do(req) 267 | if err != nil { 268 | return fmt.Errorf("request failed: %w", err) 269 | } 270 | defer resp.Body.Close() 271 | 272 | body, err := ioutil.ReadAll(resp.Body) 273 | if err != nil { 274 | return fmt.Errorf("failed to read response: %w", err) 275 | } 276 | log.Println("Response:", string(body)) 277 | 278 | // If expectedStatus is less than 100, assume it was passed in shorthand. 279 | if expectedStatus < 100 { 280 | expectedStatus *= 10 281 | } 282 | 283 | if resp.StatusCode != expectedStatus { 284 | return fmt.Errorf("unexpected status code: got %d, expected %d. Response: %s", resp.StatusCode, expectedStatus, string(body)) 285 | } 286 | 287 | return nil 288 | } 289 | 290 | // getJSONWithAuth is similar to postJSON but adds an Authorization header. 291 | func getJSONWithAuth(client *http.Client, url string, expectedStatus int, token string) error { 292 | 293 | req, err := http.NewRequest("GET", url, nil) 294 | if err != nil { 295 | return fmt.Errorf("failed to create request: %w", err) 296 | } 297 | req.Header.Set("Content-Type", "application/json") 298 | req.Header.Set("Authorization", token) 299 | 300 | resp, err := client.Do(req) 301 | if err != nil { 302 | return fmt.Errorf("request failed: %w", err) 303 | } 304 | defer resp.Body.Close() 305 | 306 | body, err := ioutil.ReadAll(resp.Body) 307 | if err != nil { 308 | return fmt.Errorf("failed to read response body: %w", err) 309 | } 310 | log.Println("Response:", string(body)) 311 | 312 | if resp.StatusCode != expectedStatus { 313 | return fmt.Errorf("unexpected status code: got %d, expected %d", resp.StatusCode, expectedStatus) 314 | } 315 | 316 | return nil 317 | } 318 | 319 | func getJSON(client *http.Client, url string, expectedStatus int) error { 320 | 321 | req, err := http.NewRequest("GET", url, nil) 322 | if err != nil { 323 | return fmt.Errorf("failed to create request: %w", err) 324 | } 325 | req.Header.Set("Content-Type", "application/json") 326 | 327 | resp, err := client.Do(req) 328 | if err != nil { 329 | return fmt.Errorf("request failed: %w", err) 330 | } 331 | defer resp.Body.Close() 332 | 333 | body, err := ioutil.ReadAll(resp.Body) 334 | if err != nil { 335 | return fmt.Errorf("failed to read response: %w", err) 336 | } 337 | log.Println("Response:", string(body)) 338 | 339 | // If expectedStatus is less than 100, assume it was passed in shorthand. 340 | if expectedStatus < 100 { 341 | expectedStatus *= 10 342 | } 343 | 344 | if resp.StatusCode != expectedStatus { 345 | return fmt.Errorf("unexpected status code: got %d, expected %d. Response: %s", resp.StatusCode, expectedStatus, string(body)) 346 | } 347 | 348 | return nil 349 | } 350 | -------------------------------------------------------------------------------- /pkg/services/authenticator.swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "authenticator.proto", 5 | "version": "version not set" 6 | }, 7 | "tags": [ 8 | { 9 | "name": "Auth" 10 | } 11 | ], 12 | "consumes": [ 13 | "application/json" 14 | ], 15 | "produces": [ 16 | "application/json" 17 | ], 18 | "paths": { 19 | "/v1/auth/login": { 20 | "post": { 21 | "operationId": "Auth_Login", 22 | "responses": { 23 | "200": { 24 | "description": "A successful response.", 25 | "schema": { 26 | "$ref": "#/definitions/authenticatorLoginReply" 27 | } 28 | }, 29 | "default": { 30 | "description": "An unexpected error response.", 31 | "schema": { 32 | "$ref": "#/definitions/rpcStatus" 33 | } 34 | } 35 | }, 36 | "parameters": [ 37 | { 38 | "name": "body", 39 | "in": "body", 40 | "required": true, 41 | "schema": { 42 | "$ref": "#/definitions/authenticatorLoginRequest" 43 | } 44 | } 45 | ], 46 | "tags": [ 47 | "Auth" 48 | ] 49 | } 50 | }, 51 | "/v1/auth/protected": { 52 | "get": { 53 | "operationId": "Auth_SampleProtected", 54 | "responses": { 55 | "200": { 56 | "description": "A successful response.", 57 | "schema": { 58 | "$ref": "#/definitions/authenticatorProtectedReply" 59 | } 60 | }, 61 | "default": { 62 | "description": "An unexpected error response.", 63 | "schema": { 64 | "$ref": "#/definitions/rpcStatus" 65 | } 66 | } 67 | }, 68 | "parameters": [ 69 | { 70 | "name": "text", 71 | "in": "query", 72 | "required": false, 73 | "type": "string" 74 | } 75 | ], 76 | "tags": [ 77 | "Auth" 78 | ] 79 | } 80 | }, 81 | "/v1/auth/register": { 82 | "post": { 83 | "operationId": "Auth_Register", 84 | "responses": { 85 | "200": { 86 | "description": "A successful response.", 87 | "schema": { 88 | "$ref": "#/definitions/authenticatorRegisterReply" 89 | } 90 | }, 91 | "default": { 92 | "description": "An unexpected error response.", 93 | "schema": { 94 | "$ref": "#/definitions/rpcStatus" 95 | } 96 | } 97 | }, 98 | "parameters": [ 99 | { 100 | "name": "body", 101 | "in": "body", 102 | "required": true, 103 | "schema": { 104 | "$ref": "#/definitions/authenticatorRegisterRequest" 105 | } 106 | } 107 | ], 108 | "tags": [ 109 | "Auth" 110 | ] 111 | } 112 | } 113 | }, 114 | "definitions": { 115 | "authenticatorLoginReply": { 116 | "type": "object", 117 | "properties": { 118 | "token": { 119 | "type": "string" 120 | } 121 | } 122 | }, 123 | "authenticatorLoginRequest": { 124 | "type": "object", 125 | "properties": { 126 | "email": { 127 | "type": "string" 128 | }, 129 | "password": { 130 | "type": "string" 131 | } 132 | } 133 | }, 134 | "authenticatorProtectedReply": { 135 | "type": "object", 136 | "properties": { 137 | "result": { 138 | "type": "string" 139 | } 140 | } 141 | }, 142 | "authenticatorRegisterReply": { 143 | "type": "object", 144 | "properties": { 145 | "reply": { 146 | "type": "string" 147 | } 148 | } 149 | }, 150 | "authenticatorRegisterRequest": { 151 | "type": "object", 152 | "properties": { 153 | "email": { 154 | "type": "string" 155 | }, 156 | "password": { 157 | "type": "string" 158 | }, 159 | "name": { 160 | "type": "string" 161 | }, 162 | "surname": { 163 | "type": "string" 164 | }, 165 | "age": { 166 | "type": "integer", 167 | "format": "int32" 168 | } 169 | } 170 | }, 171 | "protobufAny": { 172 | "type": "object", 173 | "properties": { 174 | "@type": { 175 | "type": "string" 176 | } 177 | }, 178 | "additionalProperties": {} 179 | }, 180 | "rpcStatus": { 181 | "type": "object", 182 | "properties": { 183 | "code": { 184 | "type": "integer", 185 | "format": "int32" 186 | }, 187 | "message": { 188 | "type": "string" 189 | }, 190 | "details": { 191 | "type": "array", 192 | "items": { 193 | "type": "object", 194 | "$ref": "#/definitions/protobufAny" 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /pkg/services/authenticator_server.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "db" 6 | "fmt" 7 | . "generated" 8 | 9 | "go.uber.org/zap" 10 | "golang.org/x/crypto/bcrypt" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | // AuthenticatorServer is your gRPC server. 16 | type AuthServiceServer struct { 17 | UnimplementedAuthServer 18 | PrismaClient *db.PrismaClient 19 | Logger *zap.SugaredLogger 20 | } 21 | 22 | // SampleProtected is a protected endpoint. 23 | func (s *AuthServiceServer) SampleProtected(ctx context.Context, in *ProtectedRequest) (*ProtectedReply, error) { 24 | currentUser, err := CurrentUser(ctx) 25 | if err != nil { 26 | s.Logger.Warnw("Failed to retrieve current user", "error", err) 27 | return nil, status.Errorf(codes.Unauthenticated, "failed to retrieve current user: %v", err) 28 | } 29 | return &ProtectedReply{ 30 | Result: in.Text + " " + currentUser, 31 | }, nil 32 | } 33 | 34 | // Login verifies the user's credentials and returns a JWT token. 35 | func (s *AuthServiceServer) Login(ctx context.Context, in *LoginRequest) (*LoginReply, error) { 36 | s.Logger.Infof("Login attempt for email: %s", in.Email) 37 | 38 | user, err := s.PrismaClient.User.FindUnique( 39 | db.User.Email.Equals(in.Email), 40 | ).Exec(ctx) 41 | 42 | // Handle user not found (or any error retrieving the user). 43 | if err != nil || user == nil { 44 | s.Logger.Warnw("Login failed: user not found", "email", in.Email, "error", err) 45 | return nil, status.Errorf(codes.Unauthenticated, "incorrect email or password") 46 | } 47 | 48 | // Compare the stored hashed password with the password provided. 49 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(in.Password)); err != nil { 50 | s.Logger.Warnw("Invalid password attempt", "email", in.Email) 51 | return nil, status.Errorf(codes.Unauthenticated, "Invalid credentials: %v", err) 52 | } 53 | 54 | token, err := GenerateJWT(in.Email) 55 | if err != nil { 56 | s.Logger.Errorw("Error generating token", "email", in.Email, "error", err) 57 | return nil, status.Errorf(codes.Internal, "could not generate token: %v", err) 58 | } 59 | 60 | s.Logger.Infof("Generated token for email %s", in.Email) 61 | return &LoginReply{ 62 | Token: token, 63 | }, nil 64 | } 65 | 66 | // Register creates a new user after ensuring the email is unique and hashing the password. 67 | func (s *AuthServiceServer) Register(ctx context.Context, in *RegisterRequest) (*RegisterReply, error) { 68 | // Check if a user with the given email already exists. 69 | s.Logger.Debugw("Register request received", "email", in.Email) 70 | existingUser, err := s.PrismaClient.User.FindUnique( 71 | db.User.Email.Equals(in.Email), 72 | ).Exec(ctx) 73 | if err == nil && existingUser != nil { 74 | s.Logger.Warnw("Registration failed: email already in use", "email", in.Email) 75 | return nil, status.Errorf(codes.AlreadyExists, "failed to register user: email already in use") 76 | } 77 | 78 | const bcryptCost = 12 // Recommended: 12-14 for production 79 | // Hash the password using bcrypt with the default cost. 80 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcryptCost) 81 | if err != nil { 82 | s.Logger.Errorw("Failed to hash password", "email", in.Email, "error", err) 83 | return nil, status.Errorf(codes.Internal, "failed to register user: %v", err) 84 | } 85 | 86 | obj, err := s.PrismaClient.User.CreateOne( 87 | db.User.Name.Set(in.Name), 88 | // Store the hashed password instead of plaintext. 89 | db.User.Password.Set(string(hashedPassword)), 90 | db.User.Email.Set(in.Email), 91 | db.User.Age.Set(int(in.Age)), 92 | ).Exec(ctx) 93 | if err != nil { 94 | s.Logger.Errorw("Failed to create user", "email", in.Email, "error", err) 95 | return nil, status.Errorf(codes.Internal, "failed to register user: %v", err) 96 | } 97 | 98 | s.Logger.Infow("User registered successfully", "email", obj.Email) 99 | return &RegisterReply{ 100 | Reply: fmt.Sprintf("Congratulations, User email: %s got created!", obj.Email), 101 | }, nil 102 | } 103 | -------------------------------------------------------------------------------- /pkg/services/generated/authenticator.pb.gw.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. 2 | // source: authenticator.proto 3 | 4 | /* 5 | Package generated is a reverse proxy. 6 | 7 | It translates gRPC into RESTful JSON APIs. 8 | */ 9 | package generated 10 | 11 | import ( 12 | "context" 13 | "io" 14 | "net/http" 15 | 16 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 17 | "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/grpclog" 21 | "google.golang.org/grpc/metadata" 22 | "google.golang.org/grpc/status" 23 | "google.golang.org/protobuf/proto" 24 | ) 25 | 26 | // Suppress "imported and not used" errors 27 | var _ codes.Code 28 | var _ io.Reader 29 | var _ status.Status 30 | var _ = runtime.String 31 | var _ = utilities.NewDoubleArray 32 | var _ = metadata.Join 33 | 34 | func request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 35 | var protoReq LoginRequest 36 | var metadata runtime.ServerMetadata 37 | 38 | if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { 39 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 40 | } 41 | 42 | msg, err := client.Login(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) 43 | return msg, metadata, err 44 | 45 | } 46 | 47 | func local_request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 48 | var protoReq LoginRequest 49 | var metadata runtime.ServerMetadata 50 | 51 | if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { 52 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 53 | } 54 | 55 | msg, err := server.Login(ctx, &protoReq) 56 | return msg, metadata, err 57 | 58 | } 59 | 60 | func request_Auth_Register_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 61 | var protoReq RegisterRequest 62 | var metadata runtime.ServerMetadata 63 | 64 | if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { 65 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 66 | } 67 | 68 | msg, err := client.Register(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) 69 | return msg, metadata, err 70 | 71 | } 72 | 73 | func local_request_Auth_Register_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 74 | var protoReq RegisterRequest 75 | var metadata runtime.ServerMetadata 76 | 77 | if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { 78 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 79 | } 80 | 81 | msg, err := server.Register(ctx, &protoReq) 82 | return msg, metadata, err 83 | 84 | } 85 | 86 | var ( 87 | filter_Auth_SampleProtected_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} 88 | ) 89 | 90 | func request_Auth_SampleProtected_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 91 | var protoReq ProtectedRequest 92 | var metadata runtime.ServerMetadata 93 | 94 | if err := req.ParseForm(); err != nil { 95 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 96 | } 97 | if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Auth_SampleProtected_0); err != nil { 98 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 99 | } 100 | 101 | msg, err := client.SampleProtected(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) 102 | return msg, metadata, err 103 | 104 | } 105 | 106 | func local_request_Auth_SampleProtected_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { 107 | var protoReq ProtectedRequest 108 | var metadata runtime.ServerMetadata 109 | 110 | if err := req.ParseForm(); err != nil { 111 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 112 | } 113 | if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Auth_SampleProtected_0); err != nil { 114 | return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) 115 | } 116 | 117 | msg, err := server.SampleProtected(ctx, &protoReq) 118 | return msg, metadata, err 119 | 120 | } 121 | 122 | // RegisterAuthHandlerServer registers the http handlers for service Auth to "mux". 123 | // UnaryRPC :call AuthServer directly. 124 | // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. 125 | // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthHandlerFromEndpoint instead. 126 | // GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. 127 | func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServer) error { 128 | 129 | mux.Handle("POST", pattern_Auth_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 130 | ctx, cancel := context.WithCancel(req.Context()) 131 | defer cancel() 132 | var stream runtime.ServerTransportStream 133 | ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) 134 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 135 | var err error 136 | var annotatedContext context.Context 137 | annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authenticator.Auth/Login", runtime.WithHTTPPathPattern("/v1/auth/login")) 138 | if err != nil { 139 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 140 | return 141 | } 142 | resp, md, err := local_request_Auth_Login_0(annotatedContext, inboundMarshaler, server, req, pathParams) 143 | md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) 144 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 145 | if err != nil { 146 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 147 | return 148 | } 149 | 150 | forward_Auth_Login_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 151 | 152 | }) 153 | 154 | mux.Handle("POST", pattern_Auth_Register_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 155 | ctx, cancel := context.WithCancel(req.Context()) 156 | defer cancel() 157 | var stream runtime.ServerTransportStream 158 | ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) 159 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 160 | var err error 161 | var annotatedContext context.Context 162 | annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authenticator.Auth/Register", runtime.WithHTTPPathPattern("/v1/auth/register")) 163 | if err != nil { 164 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 165 | return 166 | } 167 | resp, md, err := local_request_Auth_Register_0(annotatedContext, inboundMarshaler, server, req, pathParams) 168 | md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) 169 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 170 | if err != nil { 171 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 172 | return 173 | } 174 | 175 | forward_Auth_Register_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 176 | 177 | }) 178 | 179 | mux.Handle("GET", pattern_Auth_SampleProtected_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 180 | ctx, cancel := context.WithCancel(req.Context()) 181 | defer cancel() 182 | var stream runtime.ServerTransportStream 183 | ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) 184 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 185 | var err error 186 | var annotatedContext context.Context 187 | annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/authenticator.Auth/SampleProtected", runtime.WithHTTPPathPattern("/v1/auth/protected")) 188 | if err != nil { 189 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 190 | return 191 | } 192 | resp, md, err := local_request_Auth_SampleProtected_0(annotatedContext, inboundMarshaler, server, req, pathParams) 193 | md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) 194 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 195 | if err != nil { 196 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 197 | return 198 | } 199 | 200 | forward_Auth_SampleProtected_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 201 | 202 | }) 203 | 204 | return nil 205 | } 206 | 207 | // RegisterAuthHandlerFromEndpoint is same as RegisterAuthHandler but 208 | // automatically dials to "endpoint" and closes the connection when "ctx" gets done. 209 | func RegisterAuthHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { 210 | conn, err := grpc.NewClient(endpoint, opts...) 211 | if err != nil { 212 | return err 213 | } 214 | defer func() { 215 | if err != nil { 216 | if cerr := conn.Close(); cerr != nil { 217 | grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) 218 | } 219 | return 220 | } 221 | go func() { 222 | <-ctx.Done() 223 | if cerr := conn.Close(); cerr != nil { 224 | grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) 225 | } 226 | }() 227 | }() 228 | 229 | return RegisterAuthHandler(ctx, mux, conn) 230 | } 231 | 232 | // RegisterAuthHandler registers the http handlers for service Auth to "mux". 233 | // The handlers forward requests to the grpc endpoint over "conn". 234 | func RegisterAuthHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { 235 | return RegisterAuthHandlerClient(ctx, mux, NewAuthClient(conn)) 236 | } 237 | 238 | // RegisterAuthHandlerClient registers the http handlers for service Auth 239 | // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthClient". 240 | // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthClient" 241 | // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in 242 | // "AuthClient" to call the correct interceptors. This client ignores the HTTP middlewares. 243 | func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthClient) error { 244 | 245 | mux.Handle("POST", pattern_Auth_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 246 | ctx, cancel := context.WithCancel(req.Context()) 247 | defer cancel() 248 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 249 | var err error 250 | var annotatedContext context.Context 251 | annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authenticator.Auth/Login", runtime.WithHTTPPathPattern("/v1/auth/login")) 252 | if err != nil { 253 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 254 | return 255 | } 256 | resp, md, err := request_Auth_Login_0(annotatedContext, inboundMarshaler, client, req, pathParams) 257 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 258 | if err != nil { 259 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 260 | return 261 | } 262 | 263 | forward_Auth_Login_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 264 | 265 | }) 266 | 267 | mux.Handle("POST", pattern_Auth_Register_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 268 | ctx, cancel := context.WithCancel(req.Context()) 269 | defer cancel() 270 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 271 | var err error 272 | var annotatedContext context.Context 273 | annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authenticator.Auth/Register", runtime.WithHTTPPathPattern("/v1/auth/register")) 274 | if err != nil { 275 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 276 | return 277 | } 278 | resp, md, err := request_Auth_Register_0(annotatedContext, inboundMarshaler, client, req, pathParams) 279 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 280 | if err != nil { 281 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 282 | return 283 | } 284 | 285 | forward_Auth_Register_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 286 | 287 | }) 288 | 289 | mux.Handle("GET", pattern_Auth_SampleProtected_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { 290 | ctx, cancel := context.WithCancel(req.Context()) 291 | defer cancel() 292 | inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) 293 | var err error 294 | var annotatedContext context.Context 295 | annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/authenticator.Auth/SampleProtected", runtime.WithHTTPPathPattern("/v1/auth/protected")) 296 | if err != nil { 297 | runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) 298 | return 299 | } 300 | resp, md, err := request_Auth_SampleProtected_0(annotatedContext, inboundMarshaler, client, req, pathParams) 301 | annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) 302 | if err != nil { 303 | runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) 304 | return 305 | } 306 | 307 | forward_Auth_SampleProtected_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) 308 | 309 | }) 310 | 311 | return nil 312 | } 313 | 314 | var ( 315 | pattern_Auth_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "auth", "login"}, "")) 316 | 317 | pattern_Auth_Register_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "auth", "register"}, "")) 318 | 319 | pattern_Auth_SampleProtected_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "auth", "protected"}, "")) 320 | ) 321 | 322 | var ( 323 | forward_Auth_Login_0 = runtime.ForwardResponseMessage 324 | 325 | forward_Auth_Register_0 = runtime.ForwardResponseMessage 326 | 327 | forward_Auth_SampleProtected_0 = runtime.ForwardResponseMessage 328 | ) 329 | -------------------------------------------------------------------------------- /pkg/services/generated/authenticator_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v3.21.12 5 | // source: authenticator.proto 6 | 7 | package generated 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.64.0 or later. 19 | const _ = grpc.SupportPackageIsVersion9 20 | 21 | const ( 22 | Auth_Login_FullMethodName = "/authenticator.Auth/Login" 23 | Auth_Register_FullMethodName = "/authenticator.Auth/Register" 24 | Auth_SampleProtected_FullMethodName = "/authenticator.Auth/SampleProtected" 25 | ) 26 | 27 | // AuthClient is the client API for Auth service. 28 | // 29 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 30 | type AuthClient interface { 31 | Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginReply, error) 32 | Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterReply, error) 33 | SampleProtected(ctx context.Context, in *ProtectedRequest, opts ...grpc.CallOption) (*ProtectedReply, error) 34 | } 35 | 36 | type authClient struct { 37 | cc grpc.ClientConnInterface 38 | } 39 | 40 | func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { 41 | return &authClient{cc} 42 | } 43 | 44 | func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginReply, error) { 45 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 46 | out := new(LoginReply) 47 | err := c.cc.Invoke(ctx, Auth_Login_FullMethodName, in, out, cOpts...) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return out, nil 52 | } 53 | 54 | func (c *authClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterReply, error) { 55 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 56 | out := new(RegisterReply) 57 | err := c.cc.Invoke(ctx, Auth_Register_FullMethodName, in, out, cOpts...) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return out, nil 62 | } 63 | 64 | func (c *authClient) SampleProtected(ctx context.Context, in *ProtectedRequest, opts ...grpc.CallOption) (*ProtectedReply, error) { 65 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 66 | out := new(ProtectedReply) 67 | err := c.cc.Invoke(ctx, Auth_SampleProtected_FullMethodName, in, out, cOpts...) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return out, nil 72 | } 73 | 74 | // AuthServer is the server API for Auth service. 75 | // All implementations must embed UnimplementedAuthServer 76 | // for forward compatibility. 77 | type AuthServer interface { 78 | Login(context.Context, *LoginRequest) (*LoginReply, error) 79 | Register(context.Context, *RegisterRequest) (*RegisterReply, error) 80 | SampleProtected(context.Context, *ProtectedRequest) (*ProtectedReply, error) 81 | mustEmbedUnimplementedAuthServer() 82 | } 83 | 84 | // UnimplementedAuthServer must be embedded to have 85 | // forward compatible implementations. 86 | // 87 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 88 | // pointer dereference when methods are called. 89 | type UnimplementedAuthServer struct{} 90 | 91 | func (UnimplementedAuthServer) Login(context.Context, *LoginRequest) (*LoginReply, error) { 92 | return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") 93 | } 94 | func (UnimplementedAuthServer) Register(context.Context, *RegisterRequest) (*RegisterReply, error) { 95 | return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") 96 | } 97 | func (UnimplementedAuthServer) SampleProtected(context.Context, *ProtectedRequest) (*ProtectedReply, error) { 98 | return nil, status.Errorf(codes.Unimplemented, "method SampleProtected not implemented") 99 | } 100 | func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {} 101 | func (UnimplementedAuthServer) testEmbeddedByValue() {} 102 | 103 | // UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. 104 | // Use of this interface is not recommended, as added methods to AuthServer will 105 | // result in compilation errors. 106 | type UnsafeAuthServer interface { 107 | mustEmbedUnimplementedAuthServer() 108 | } 109 | 110 | func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) { 111 | // If the following call pancis, it indicates UnimplementedAuthServer was 112 | // embedded by pointer and is nil. This will cause panics if an 113 | // unimplemented method is ever invoked, so we test this at initialization 114 | // time to prevent it from happening at runtime later due to I/O. 115 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 116 | t.testEmbeddedByValue() 117 | } 118 | s.RegisterService(&Auth_ServiceDesc, srv) 119 | } 120 | 121 | func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 122 | in := new(LoginRequest) 123 | if err := dec(in); err != nil { 124 | return nil, err 125 | } 126 | if interceptor == nil { 127 | return srv.(AuthServer).Login(ctx, in) 128 | } 129 | info := &grpc.UnaryServerInfo{ 130 | Server: srv, 131 | FullMethod: Auth_Login_FullMethodName, 132 | } 133 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 134 | return srv.(AuthServer).Login(ctx, req.(*LoginRequest)) 135 | } 136 | return interceptor(ctx, in, info, handler) 137 | } 138 | 139 | func _Auth_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 140 | in := new(RegisterRequest) 141 | if err := dec(in); err != nil { 142 | return nil, err 143 | } 144 | if interceptor == nil { 145 | return srv.(AuthServer).Register(ctx, in) 146 | } 147 | info := &grpc.UnaryServerInfo{ 148 | Server: srv, 149 | FullMethod: Auth_Register_FullMethodName, 150 | } 151 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 152 | return srv.(AuthServer).Register(ctx, req.(*RegisterRequest)) 153 | } 154 | return interceptor(ctx, in, info, handler) 155 | } 156 | 157 | func _Auth_SampleProtected_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 158 | in := new(ProtectedRequest) 159 | if err := dec(in); err != nil { 160 | return nil, err 161 | } 162 | if interceptor == nil { 163 | return srv.(AuthServer).SampleProtected(ctx, in) 164 | } 165 | info := &grpc.UnaryServerInfo{ 166 | Server: srv, 167 | FullMethod: Auth_SampleProtected_FullMethodName, 168 | } 169 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 170 | return srv.(AuthServer).SampleProtected(ctx, req.(*ProtectedRequest)) 171 | } 172 | return interceptor(ctx, in, info, handler) 173 | } 174 | 175 | // Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. 176 | // It's only intended for direct use with grpc.RegisterService, 177 | // and not to be introspected or modified (even as a copy) 178 | var Auth_ServiceDesc = grpc.ServiceDesc{ 179 | ServiceName: "authenticator.Auth", 180 | HandlerType: (*AuthServer)(nil), 181 | Methods: []grpc.MethodDesc{ 182 | { 183 | MethodName: "Login", 184 | Handler: _Auth_Login_Handler, 185 | }, 186 | { 187 | MethodName: "Register", 188 | Handler: _Auth_Register_Handler, 189 | }, 190 | { 191 | MethodName: "SampleProtected", 192 | Handler: _Auth_SampleProtected_Handler, 193 | }, 194 | }, 195 | Streams: []grpc.StreamDesc{}, 196 | Metadata: "authenticator.proto", 197 | } 198 | -------------------------------------------------------------------------------- /pkg/services/generated/authenticator_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: authenticator_grpc.pb.go 3 | 4 | // Package mock_generated is a generated GoMock package. 5 | package generated 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | grpc "google.golang.org/grpc" 13 | ) 14 | 15 | // MockAuthClient is a mock of AuthClient interface. 16 | type MockAuthClient struct { 17 | ctrl *gomock.Controller 18 | recorder *MockAuthClientMockRecorder 19 | } 20 | 21 | // MockAuthClientMockRecorder is the mock recorder for MockAuthClient. 22 | type MockAuthClientMockRecorder struct { 23 | mock *MockAuthClient 24 | } 25 | 26 | // NewMockAuthClient creates a new mock instance. 27 | func NewMockAuthClient(ctrl *gomock.Controller) *MockAuthClient { 28 | mock := &MockAuthClient{ctrl: ctrl} 29 | mock.recorder = &MockAuthClientMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockAuthClient) EXPECT() *MockAuthClientMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Login mocks base method. 39 | func (m *MockAuthClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginReply, error) { 40 | m.ctrl.T.Helper() 41 | varargs := []interface{}{ctx, in} 42 | for _, a := range opts { 43 | varargs = append(varargs, a) 44 | } 45 | ret := m.ctrl.Call(m, "Login", varargs...) 46 | ret0, _ := ret[0].(*LoginReply) 47 | ret1, _ := ret[1].(error) 48 | return ret0, ret1 49 | } 50 | 51 | // Login indicates an expected call of Login. 52 | func (mr *MockAuthClientMockRecorder) Login(ctx, in interface{}, opts ...interface{}) *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | varargs := append([]interface{}{ctx, in}, opts...) 55 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockAuthClient)(nil).Login), varargs...) 56 | } 57 | 58 | // Register mocks base method. 59 | func (m *MockAuthClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterReply, error) { 60 | m.ctrl.T.Helper() 61 | varargs := []interface{}{ctx, in} 62 | for _, a := range opts { 63 | varargs = append(varargs, a) 64 | } 65 | ret := m.ctrl.Call(m, "Register", varargs...) 66 | ret0, _ := ret[0].(*RegisterReply) 67 | ret1, _ := ret[1].(error) 68 | return ret0, ret1 69 | } 70 | 71 | // Register indicates an expected call of Register. 72 | func (mr *MockAuthClientMockRecorder) Register(ctx, in interface{}, opts ...interface{}) *gomock.Call { 73 | mr.mock.ctrl.T.Helper() 74 | varargs := append([]interface{}{ctx, in}, opts...) 75 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockAuthClient)(nil).Register), varargs...) 76 | } 77 | 78 | // SampleProtected mocks base method. 79 | func (m *MockAuthClient) SampleProtected(ctx context.Context, in *ProtectedRequest, opts ...grpc.CallOption) (*ProtectedReply, error) { 80 | m.ctrl.T.Helper() 81 | varargs := []interface{}{ctx, in} 82 | for _, a := range opts { 83 | varargs = append(varargs, a) 84 | } 85 | ret := m.ctrl.Call(m, "SampleProtected", varargs...) 86 | ret0, _ := ret[0].(*ProtectedReply) 87 | ret1, _ := ret[1].(error) 88 | return ret0, ret1 89 | } 90 | 91 | // SampleProtected indicates an expected call of SampleProtected. 92 | func (mr *MockAuthClientMockRecorder) SampleProtected(ctx, in interface{}, opts ...interface{}) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | varargs := append([]interface{}{ctx, in}, opts...) 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SampleProtected", reflect.TypeOf((*MockAuthClient)(nil).SampleProtected), varargs...) 96 | } 97 | 98 | // MockAuthServer is a mock of AuthServer interface. 99 | type MockAuthServer struct { 100 | ctrl *gomock.Controller 101 | recorder *MockAuthServerMockRecorder 102 | } 103 | 104 | // MockAuthServerMockRecorder is the mock recorder for MockAuthServer. 105 | type MockAuthServerMockRecorder struct { 106 | mock *MockAuthServer 107 | } 108 | 109 | // NewMockAuthServer creates a new mock instance. 110 | func NewMockAuthServer(ctrl *gomock.Controller) *MockAuthServer { 111 | mock := &MockAuthServer{ctrl: ctrl} 112 | mock.recorder = &MockAuthServerMockRecorder{mock} 113 | return mock 114 | } 115 | 116 | // EXPECT returns an object that allows the caller to indicate expected use. 117 | func (m *MockAuthServer) EXPECT() *MockAuthServerMockRecorder { 118 | return m.recorder 119 | } 120 | 121 | // Login mocks base method. 122 | func (m *MockAuthServer) Login(arg0 context.Context, arg1 *LoginRequest) (*LoginReply, error) { 123 | m.ctrl.T.Helper() 124 | ret := m.ctrl.Call(m, "Login", arg0, arg1) 125 | ret0, _ := ret[0].(*LoginReply) 126 | ret1, _ := ret[1].(error) 127 | return ret0, ret1 128 | } 129 | 130 | // Login indicates an expected call of Login. 131 | func (mr *MockAuthServerMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call { 132 | mr.mock.ctrl.T.Helper() 133 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockAuthServer)(nil).Login), arg0, arg1) 134 | } 135 | 136 | // Register mocks base method. 137 | func (m *MockAuthServer) Register(arg0 context.Context, arg1 *RegisterRequest) (*RegisterReply, error) { 138 | m.ctrl.T.Helper() 139 | ret := m.ctrl.Call(m, "Register", arg0, arg1) 140 | ret0, _ := ret[0].(*RegisterReply) 141 | ret1, _ := ret[1].(error) 142 | return ret0, ret1 143 | } 144 | 145 | // Register indicates an expected call of Register. 146 | func (mr *MockAuthServerMockRecorder) Register(arg0, arg1 interface{}) *gomock.Call { 147 | mr.mock.ctrl.T.Helper() 148 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockAuthServer)(nil).Register), arg0, arg1) 149 | } 150 | 151 | // SampleProtected mocks base method. 152 | func (m *MockAuthServer) SampleProtected(arg0 context.Context, arg1 *ProtectedRequest) (*ProtectedReply, error) { 153 | m.ctrl.T.Helper() 154 | ret := m.ctrl.Call(m, "SampleProtected", arg0, arg1) 155 | ret0, _ := ret[0].(*ProtectedReply) 156 | ret1, _ := ret[1].(error) 157 | return ret0, ret1 158 | } 159 | 160 | // SampleProtected indicates an expected call of SampleProtected. 161 | func (mr *MockAuthServerMockRecorder) SampleProtected(arg0, arg1 interface{}) *gomock.Call { 162 | mr.mock.ctrl.T.Helper() 163 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SampleProtected", reflect.TypeOf((*MockAuthServer)(nil).SampleProtected), arg0, arg1) 164 | } 165 | 166 | // mustEmbedUnimplementedAuthServer mocks base method. 167 | func (m *MockAuthServer) mustEmbedUnimplementedAuthServer() { 168 | m.ctrl.T.Helper() 169 | m.ctrl.Call(m, "mustEmbedUnimplementedAuthServer") 170 | } 171 | 172 | // mustEmbedUnimplementedAuthServer indicates an expected call of mustEmbedUnimplementedAuthServer. 173 | func (mr *MockAuthServerMockRecorder) mustEmbedUnimplementedAuthServer() *gomock.Call { 174 | mr.mock.ctrl.T.Helper() 175 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedAuthServer", reflect.TypeOf((*MockAuthServer)(nil).mustEmbedUnimplementedAuthServer)) 176 | } 177 | 178 | // MockUnsafeAuthServer is a mock of UnsafeAuthServer interface. 179 | type MockUnsafeAuthServer struct { 180 | ctrl *gomock.Controller 181 | recorder *MockUnsafeAuthServerMockRecorder 182 | } 183 | 184 | // MockUnsafeAuthServerMockRecorder is the mock recorder for MockUnsafeAuthServer. 185 | type MockUnsafeAuthServerMockRecorder struct { 186 | mock *MockUnsafeAuthServer 187 | } 188 | 189 | // NewMockUnsafeAuthServer creates a new mock instance. 190 | func NewMockUnsafeAuthServer(ctrl *gomock.Controller) *MockUnsafeAuthServer { 191 | mock := &MockUnsafeAuthServer{ctrl: ctrl} 192 | mock.recorder = &MockUnsafeAuthServerMockRecorder{mock} 193 | return mock 194 | } 195 | 196 | // EXPECT returns an object that allows the caller to indicate expected use. 197 | func (m *MockUnsafeAuthServer) EXPECT() *MockUnsafeAuthServerMockRecorder { 198 | return m.recorder 199 | } 200 | 201 | // mustEmbedUnimplementedAuthServer mocks base method. 202 | func (m *MockUnsafeAuthServer) mustEmbedUnimplementedAuthServer() { 203 | m.ctrl.T.Helper() 204 | m.ctrl.Call(m, "mustEmbedUnimplementedAuthServer") 205 | } 206 | 207 | // mustEmbedUnimplementedAuthServer indicates an expected call of mustEmbedUnimplementedAuthServer. 208 | func (mr *MockUnsafeAuthServerMockRecorder) mustEmbedUnimplementedAuthServer() *gomock.Call { 209 | mr.mock.ctrl.T.Helper() 210 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedAuthServer", reflect.TypeOf((*MockUnsafeAuthServer)(nil).mustEmbedUnimplementedAuthServer)) 211 | } 212 | -------------------------------------------------------------------------------- /pkg/services/generated/authenticator_server_test.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | reflect "reflect" 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRegisterAndLogin(t *testing.T) { 15 | ctrl := gomock.NewController(t) 16 | defer ctrl.Finish() 17 | 18 | mockAuthClient := NewMockAuthClient(ctrl) 19 | 20 | // Valid registration request 21 | regReq := &RegisterRequest{ 22 | Email: "john_doe@example.com", 23 | Password: "password", 24 | Name: "test", 25 | Surname: "test", 26 | Age: 27, 27 | } 28 | regReply := fmt.Sprintf("Congratulations, User email: %s got created!", "john_doe@example.com") 29 | expectedRegRes := &RegisterReply{Reply: regReply} 30 | 31 | mockAuthClient.EXPECT().Register(gomock.Any(), gomock.Eq(regReq), gomock.Any()).Return(expectedRegRes, nil) 32 | 33 | // Call Register 34 | regRes, regErr := mockAuthClient.Register(context.Background(), regReq) 35 | assert.NoError(t, regErr) 36 | assert.Equal(t, expectedRegRes, regRes) 37 | 38 | // Valid login request 39 | loginReq := &LoginRequest{Email: "john_doe@example.com", Password: "password"} 40 | expectedLoginRes := &LoginReply{Token: "some-jwt-token"} 41 | 42 | mockAuthClient.EXPECT().Login(gomock.Any(), gomock.Eq(loginReq), gomock.Any()).Return(expectedLoginRes, nil) 43 | 44 | // Call Login 45 | loginRes, loginErr := mockAuthClient.Login(context.Background(), loginReq) 46 | assert.NoError(t, loginErr) 47 | assert.Equal(t, expectedLoginRes, loginRes) 48 | } 49 | 50 | func TestRegisterWithEmptyFields(t *testing.T) { 51 | ctrl := gomock.NewController(t) 52 | defer ctrl.Finish() 53 | 54 | mockAuthClient := NewMockAuthClient(ctrl) 55 | 56 | invalidReq := &RegisterRequest{ 57 | Email: "", 58 | Password: "", 59 | Name: "test", 60 | Surname: "test", 61 | Age: 27, 62 | } 63 | mockAuthClient.EXPECT().Register(gomock.Any(), gomock.Eq(invalidReq), gomock.Any()). 64 | Return(nil, fmt.Errorf("email and password cannot be empty")) 65 | 66 | // Call Register with empty fields 67 | res, err := mockAuthClient.Register(context.Background(), invalidReq) 68 | assert.Error(t, err) 69 | assert.Nil(t, res) 70 | } 71 | 72 | func TestLoginWithIncorrectPassword(t *testing.T) { 73 | ctrl := gomock.NewController(t) 74 | defer ctrl.Finish() 75 | 76 | mockAuthClient := NewMockAuthClient(ctrl) 77 | 78 | invalidLoginReq := &LoginRequest{Email: "john_doe@example.com", Password: "wrongpass"} 79 | 80 | mockAuthClient.EXPECT().Login(gomock.Any(), gomock.Eq(invalidLoginReq), gomock.Any()). 81 | Return(nil, fmt.Errorf("invalid credentials")) 82 | 83 | // Call Login with wrong password 84 | res, err := mockAuthClient.Login(context.Background(), invalidLoginReq) 85 | assert.Error(t, err) 86 | assert.Nil(t, res) 87 | } 88 | 89 | func TestRegisterExistingUser(t *testing.T) { 90 | ctrl := gomock.NewController(t) 91 | defer ctrl.Finish() 92 | 93 | mockAuthClient := NewMockAuthClient(ctrl) 94 | 95 | existingUserReq := &RegisterRequest{ 96 | Email: "john_doe@example.com", 97 | Password: "password", 98 | Name: "test", 99 | Surname: "test", 100 | Age: 27, 101 | } 102 | 103 | mockAuthClient.EXPECT().Register(gomock.Any(), gomock.Eq(existingUserReq), gomock.Any()). 104 | Return(nil, fmt.Errorf("user already exists")) 105 | 106 | // Attempt to register the same user twice 107 | res, err := mockAuthClient.Register(context.Background(), existingUserReq) 108 | assert.Error(t, err) 109 | assert.Nil(t, res) 110 | } 111 | 112 | func TestSampleProtectedSuccess(t *testing.T) { 113 | ctrl := gomock.NewController(t) 114 | defer ctrl.Finish() 115 | 116 | mockAuthClient := NewMockAuthClient(ctrl) 117 | 118 | expectedRes := &ProtectedReply{Result: "Access Granted"} 119 | mockAuthClient.EXPECT().SampleProtected(gomock.Any(), gomock.Any()).Return(expectedRes, nil) 120 | 121 | // Call SampleProtected 122 | res, err := mockAuthClient.SampleProtected(context.Background(), &ProtectedRequest{}) 123 | assert.NoError(t, err) 124 | assert.Equal(t, expectedRes, res) 125 | } 126 | 127 | func TestSampleProtectedFailure(t *testing.T) { 128 | ctrl := gomock.NewController(t) 129 | defer ctrl.Finish() 130 | 131 | mockAuthClient := NewMockAuthClient(ctrl) 132 | 133 | mockAuthClient.EXPECT().SampleProtected(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("authentication failed")) 134 | 135 | // Call SampleProtected 136 | res, err := mockAuthClient.SampleProtected(context.Background(), &ProtectedRequest{}) 137 | assert.Error(t, err) 138 | assert.Nil(t, res) 139 | } 140 | 141 | // TestNewMockAuthServer verifies that NewMockAuthServer properly creates an instance and initializes its recorder. 142 | func TestNewMockAuthServer(t *testing.T) { 143 | ctrl := gomock.NewController(t) 144 | defer ctrl.Finish() 145 | 146 | mockServer := NewMockAuthServer(ctrl) 147 | if mockServer == nil { 148 | t.Fatal("NewMockAuthServer returned nil") 149 | } 150 | if mockServer.recorder == nil { 151 | t.Error("NewMockAuthServer did not initialize recorder") 152 | } 153 | } 154 | 155 | // TestEXPECT checks that calling EXPECT() on the mock returns a valid recorder. 156 | func TestEXPECT(t *testing.T) { 157 | ctrl := gomock.NewController(t) 158 | defer ctrl.Finish() 159 | 160 | mockServer := NewMockAuthServer(ctrl) 161 | rec := mockServer.EXPECT() 162 | if rec == nil { 163 | t.Fatal("EXPECT() returned nil") 164 | } 165 | } 166 | 167 | // TestLogin sets an expectation for Login and then calls it. 168 | // It verifies that the returned reply and error match the expected values. 169 | func TestLogin(t *testing.T) { 170 | ctrl := gomock.NewController(t) 171 | defer ctrl.Finish() 172 | 173 | mockServer := NewMockAuthServer(ctrl) 174 | 175 | // Create a dummy login request and expected reply. 176 | req := &LoginRequest{} 177 | expectedReply := &LoginReply{} 178 | expectedErr := error(nil) 179 | 180 | // Set expectation: Login should be called with any context and our req, and return expectedReply and no error. 181 | mockServer.EXPECT().Login(gomock.Any(), req).Return(expectedReply, expectedErr) 182 | 183 | // Call Login. 184 | reply, err := mockServer.Login(context.Background(), req) 185 | if err != expectedErr { 186 | t.Errorf("Expected error %v, got %v", expectedErr, err) 187 | } 188 | if reply != expectedReply { 189 | t.Errorf("Expected reply %v, got %v", expectedReply, reply) 190 | } 191 | } 192 | 193 | // TestRegister sets an expectation for Register and then calls it. 194 | // It checks that the returned reply and error are as expected. 195 | func TestRegister(t *testing.T) { 196 | ctrl := gomock.NewController(t) 197 | defer ctrl.Finish() 198 | 199 | mockServer := NewMockAuthServer(ctrl) 200 | 201 | req := &RegisterRequest{} 202 | expectedReply := &RegisterReply{} 203 | expectedErr := errors.New("registration error") 204 | 205 | // Set expectation: Register should be called with any context and our req, and return expectedReply and a dummy error. 206 | mockServer.EXPECT().Register(gomock.Any(), req).Return(expectedReply, expectedErr) 207 | 208 | reply, err := mockServer.Register(context.Background(), req) 209 | if err == nil { 210 | t.Error("Expected an error, got nil") 211 | } else if err.Error() != expectedErr.Error() { 212 | t.Errorf("Expected error %v, got %v", expectedErr, err) 213 | } 214 | if reply != expectedReply { 215 | t.Errorf("Expected reply %v, got %v", expectedReply, reply) 216 | } 217 | } 218 | 219 | func TestNewMockUnsafeAuthServer(t *testing.T) { 220 | ctrl := gomock.NewController(t) 221 | defer ctrl.Finish() 222 | mock := NewMockUnsafeAuthServer(ctrl) 223 | if mock == nil { 224 | t.Fatal("NewMockUnsafeAuthServer returned nil") 225 | } 226 | } 227 | 228 | // TestEXPECT verifies that calling EXPECT on the mock returns a non-nil expectation object. 229 | func TestEXPECTMockUnsafeAuthServer(t *testing.T) { 230 | ctrl := gomock.NewController(t) 231 | defer ctrl.Finish() 232 | mock := NewMockUnsafeAuthServer(ctrl) 233 | expect := mock.EXPECT() 234 | if expect == nil { 235 | t.Fatal("EXPECT returned nil") 236 | } 237 | } 238 | 239 | func TestSampleProtected_Authorized(t *testing.T) { 240 | ctrl := gomock.NewController(t) 241 | defer ctrl.Finish() 242 | 243 | mockServer := NewMockAuthServer(ctrl) 244 | // Create a valid protected request; adjust the fields as needed. 245 | req := &ProtectedRequest{Text: "valid-token-text"} 246 | expectedReply := &ProtectedReply{Result: "Access Granted"} 247 | 248 | // Set up expectation: when SampleProtected is called with the valid request, 249 | // it should return the expected reply and no error. 250 | mockServer.EXPECT().SampleProtected(gomock.Any(), gomock.Eq(req)).Return(expectedReply, nil) 251 | 252 | // Call SampleProtected. 253 | reply, err := mockServer.SampleProtected(context.Background(), req) 254 | if err != nil { 255 | t.Fatalf("Expected no error, got: %v", err) 256 | } 257 | if !reflect.DeepEqual(reply, expectedReply) { 258 | t.Fatalf("Expected reply %+v, got %+v", expectedReply, reply) 259 | } 260 | } 261 | 262 | // TestSampleProtected_Unauthorized verifies that SampleProtected returns an error 263 | // when provided with invalid credentials. 264 | func TestSampleProtected_Unauthorized(t *testing.T) { 265 | ctrl := gomock.NewController(t) 266 | defer ctrl.Finish() 267 | 268 | mockServer := NewMockAuthServer(ctrl) 269 | // Create a request with an invalid token. 270 | req := &ProtectedRequest{Text: "invalid-token-text"} 271 | 272 | // Set up expectation: when SampleProtected is called with the invalid request, 273 | // it should return nil and an "unauthorized" error. 274 | mockServer.EXPECT().SampleProtected(gomock.Any(), gomock.Eq(req)).Return(nil, errors.New("unauthorized")) 275 | 276 | // Call SampleProtected. 277 | reply, err := mockServer.SampleProtected(context.Background(), req) 278 | if err == nil || err.Error() != "unauthorized" { 279 | t.Fatalf("Expected 'unauthorized' error, got error: %v", err) 280 | } 281 | if reply != nil { 282 | t.Fatalf("Expected nil reply, got: %+v", reply) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /pkg/services/generated/go.mod: -------------------------------------------------------------------------------- 1 | module generated 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 8 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb 9 | google.golang.org/grpc v1.71.0 10 | google.golang.org/protobuf v1.36.5 11 | ) 12 | 13 | require ( 14 | golang.org/x/net v0.35.0 // indirect 15 | golang.org/x/sys v0.30.0 // indirect 16 | golang.org/x/text v0.22.0 // indirect 17 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/services/generated/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 2 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 3 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 4 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 5 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 6 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 7 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 8 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 9 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 10 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 14 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 15 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 16 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 17 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 18 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 19 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 20 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 21 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 22 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 23 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 24 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 25 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 26 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 27 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 28 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 29 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 30 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 31 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 32 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 34 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 35 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 36 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 37 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 44 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 48 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 49 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 50 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 53 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= 57 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= 58 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= 59 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 60 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 61 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 62 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 63 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 64 | -------------------------------------------------------------------------------- /pkg/services/go.mod: -------------------------------------------------------------------------------- 1 | module services 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/golang-jwt/jwt/v5 v5.2.1 9 | github.com/stretchr/testify v1.10.0 10 | github.com/testcontainers/testcontainers-go v0.35.0 11 | go.uber.org/zap v1.27.0 12 | golang.org/x/crypto v0.36.0 13 | google.golang.org/grpc v1.71.0 14 | ) 15 | 16 | require ( 17 | dario.cat/mergo v1.0.0 // indirect 18 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 19 | github.com/Microsoft/go-winio v0.6.2 // indirect 20 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 21 | github.com/containerd/containerd v1.7.18 // indirect 22 | github.com/containerd/log v0.1.0 // indirect 23 | github.com/containerd/platforms v0.2.1 // indirect 24 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/distribution/reference v0.6.0 // indirect 27 | github.com/docker/docker v27.1.1+incompatible // indirect 28 | github.com/docker/go-connections v0.5.0 // indirect 29 | github.com/docker/go-units v0.5.0 // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/go-logr/logr v1.4.2 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/go-ole/go-ole v1.2.6 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/klauspost/compress v1.17.4 // indirect 37 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 38 | github.com/magiconair/properties v1.8.7 // indirect 39 | github.com/moby/docker-image-spec v1.3.1 // indirect 40 | github.com/moby/patternmatcher v0.6.0 // indirect 41 | github.com/moby/sys/sequential v0.5.0 // indirect 42 | github.com/moby/sys/user v0.1.0 // indirect 43 | github.com/moby/term v0.5.0 // indirect 44 | github.com/morikuni/aec v1.0.0 // indirect 45 | github.com/opencontainers/go-digest v1.0.0 // indirect 46 | github.com/opencontainers/image-spec v1.1.0 // indirect 47 | github.com/pkg/errors v0.9.1 // indirect 48 | github.com/pmezard/go-difflib v1.0.0 // indirect 49 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 50 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 51 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 52 | github.com/sirupsen/logrus v1.9.3 // indirect 53 | github.com/tklauser/go-sysconf v0.3.12 // indirect 54 | github.com/tklauser/numcpus v0.6.1 // indirect 55 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 56 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 57 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 58 | go.opentelemetry.io/otel v1.34.0 // indirect 59 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 60 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 61 | go.uber.org/multierr v1.10.0 // indirect 62 | golang.org/x/sys v0.31.0 // indirect 63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 64 | google.golang.org/protobuf v1.36.4 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /pkg/services/jwt.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/golang-jwt/jwt/v5" 10 | "google.golang.org/grpc/metadata" 11 | ) 12 | 13 | // getJWTSecret fetches the JWT secret securely from environment variables. 14 | func getJWTSecret() ([]byte, error) { 15 | secret := os.Getenv("JWT_SECRET") 16 | if secret == "" { 17 | return nil, fmt.Errorf("JWT_SECRET is not set in environment variables") 18 | } 19 | return []byte(secret), nil 20 | } 21 | 22 | // Claims struct for JWT payload with added issuer claim. 23 | type Claims struct { 24 | Email string `json:"email"` 25 | jwt.RegisteredClaims 26 | } 27 | 28 | // NewClaims creates a new Claims object with expiration time and an issuer. 29 | func NewClaims(email string) *Claims { 30 | expirationTime := time.Now().Add(24 * time.Hour) 31 | issuer := os.Getenv("JWT_ISSUER") 32 | if issuer == "" { 33 | issuer = "default-issuer" // Fallback issuer, but best to set it in env. 34 | } 35 | return &Claims{ 36 | Email: email, 37 | RegisteredClaims: jwt.RegisteredClaims{ 38 | ExpiresAt: jwt.NewNumericDate(expirationTime), 39 | Issuer: issuer, 40 | // Optionally, add Audience, Subject, etc. 41 | }, 42 | } 43 | } 44 | 45 | // GenerateJWT generates a JWT token securely with additional claims. 46 | func GenerateJWT(email string) (string, error) { 47 | secret, err := getJWTSecret() 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | claims := NewClaims(email) 53 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 54 | 55 | tokenString, err := token.SignedString(secret) 56 | if err != nil { 57 | return "", fmt.Errorf("failed to sign token: %v", err) 58 | } 59 | return tokenString, nil 60 | } 61 | 62 | // VerifyJWT verifies a JWT token, strictly checks the signing method, and extracts claims. 63 | func VerifyJWT(tokenStr string) (*Claims, error) { 64 | secret, err := getJWTSecret() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | claims := &Claims{} 70 | token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { 71 | // Ensure the token method conforms to expected signing method 72 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 73 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 74 | } 75 | return secret, nil 76 | }) 77 | if err != nil || !token.Valid { 78 | return nil, fmt.Errorf("invalid or expired token") 79 | } 80 | return claims, nil 81 | } 82 | 83 | // CurrentUser extracts the user email from context metadata safely. 84 | func CurrentUser(ctx context.Context) (string, error) { 85 | md, ok := metadata.FromIncomingContext(ctx) 86 | if !ok { 87 | return "", fmt.Errorf("missing metadata") 88 | } 89 | 90 | currentUser, exists := md["current_user"] 91 | if !exists || len(currentUser) == 0 { 92 | return "", fmt.Errorf("current_user metadata is missing") 93 | } 94 | return currentUser[0], nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/services/jwt_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "google.golang.org/grpc/metadata" 9 | ) 10 | 11 | func TestGenerateJWT(t *testing.T) { 12 | email := "test@example.com" 13 | token, err := GenerateJWT(email) 14 | 15 | assert.NoError(t, err, "expected no error from GenerateJWT") 16 | assert.NotEmpty(t, token, "expected token to be non-empty") 17 | 18 | // Verify token can be parsed and contains correct email 19 | claims, err := VerifyJWT(token) 20 | assert.NoError(t, err, "expected no error from VerifyJWT") 21 | assert.Equal(t, email, claims.Email, "expected email in claims to match") 22 | } 23 | 24 | func TestVerifyJWT(t *testing.T) { 25 | email := "verify@test.com" 26 | token, _ := GenerateJWT(email) 27 | 28 | claims, err := VerifyJWT(token) 29 | assert.NoError(t, err, "expected no error from VerifyJWT") 30 | assert.Equal(t, email, claims.Email, "expected email in claims to match") 31 | 32 | // Test with invalid token 33 | _, err = VerifyJWT("invalid.token") 34 | assert.Error(t, err, "expected error from VerifyJWT with invalid token") 35 | } 36 | 37 | func TestCurrentUser(t *testing.T) { 38 | email := "user@test.com" 39 | ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("current_user", email)) 40 | 41 | currentUser, err := CurrentUser(ctx) 42 | assert.NoError(t, err, "expected no error from CurrentUser") 43 | assert.Equal(t, email, currentUser, "expected current user to match email") 44 | 45 | // Test without metadata 46 | _, err = CurrentUser(context.Background()) 47 | assert.Error(t, err, "expected error from CurrentUser with missing metadata") 48 | } 49 | -------------------------------------------------------------------------------- /services.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ServiceName": "Auth", 4 | "ServiceStruct": "AuthServiceServer", 5 | "ServiceRegister": "RegisterAuthServer", 6 | "HandlerRegister": "RegisterAuthHandler" 7 | } 8 | ] 9 | --------------------------------------------------------------------------------