├── .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 |
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 | [](https://libs.tech/project/882664523/thunder)
11 | [](https://golang.org)
12 | [](LICENSE)
13 | [](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 | 
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 |
--------------------------------------------------------------------------------