Skip to content

Commit 4d306e6

Browse files
committed
chore: fix resolver
1 parent 2d5c9ac commit 4d306e6

2 files changed

Lines changed: 302 additions & 3 deletions

File tree

docs/grpc-rest-api-spec.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# Specification: gRPC & REST API with gRPC-Gateway
2+
3+
This document specifies the architecture and mapping for exposing Authorizer's public APIs via gRPC and a generated RESTful layer using `grpc-gateway`.
4+
5+
## 1. Architecture Overview
6+
7+
Authorizer uses a single-source-of-truth Protobuf definition to generate:
8+
1. **gRPC Service**: For high-performance, typed backend-to-backend communication.
9+
2. **RESTful API**: Via `grpc-gateway`, maintaining backward compatibility and ease of use for web/mobile clients.
10+
3. **TypeScript Clients**: Via `ts-proto`, providing type-safe API consumption out of the box.
11+
12+
### Package & Versioning
13+
- **Package**: `authorizer.v1`
14+
- **Directory Structure**: `proto/authorizer/v1/`
15+
- **API Versioning**: Hardcoded in HTTP paths as `/api/v1/...` and tracked via Protobuf package versioning.
16+
17+
### Ecosystem Tooling
18+
- **`buf`**: Managed build system for Protobuf.
19+
- **`protoc-gen-grpc-gateway`**: Generates the reverse proxy from gRPC to REST.
20+
- **`protoc-gen-openapiv2`**: Generates Swagger/OpenAPI documentation.
21+
- **`protoc-gen-ts_proto`**: Generates the TypeScript client.
22+
- **`protovalidate`**: High-performance validation rules using Common Expression Language (CEL).
23+
24+
---
25+
26+
## 2. API Mapping (Public Surface)
27+
28+
All public GraphQL queries and mutations are mapped to RPC methods. Terminologies are preserved from the GraphQL schema.
29+
30+
### 2.1. Authentication Service (`authorizer.v1.AuthorizerService`)
31+
32+
| RPC Method | GraphQL Equivalent | HTTP Path | Permissions |
33+
| :--- | :--- | :--- | :--- |
34+
| `Signup` | `signup` | `POST /api/v1/signup` | Public |
35+
| `Login` | `login` | `POST /api/v1/login` | Public |
36+
| `MagicLinkLogin` | `magic_link_login` | `POST /api/v1/magic-link-login` | Public |
37+
| `Logout` | `logout` | `POST /api/v1/logout` | Authenticated |
38+
| `VerifyEmail` | `verify_email` | `POST /api/v1/verify-email` | Public |
39+
| `ResendVerifyEmail` | `resend_verify_email` | `POST /api/v1/resend-verify-email` | Public |
40+
| `ForgotPassword` | `forgot_password` | `POST /api/v1/forgot-password` | Public |
41+
| `ResetPassword` | `reset_password` | `POST /api/v1/reset-password` | Public |
42+
| `VerifyOtp` | `verify_otp` | `POST /api/v1/verify-otp` | Public |
43+
| `ResendOtp` | `resend_otp` | `POST /api/v1/resend-otp` | Public |
44+
| `Revoke` | `revoke` | `POST /api/v1/revoke` | Authenticated |
45+
| `DeactivateAccount` | `deactivate_account` | `DELETE /api/v1/account` | Authenticated |
46+
| `GetMeta` | `meta` | `GET /api/v1/meta` | Public |
47+
| `GetSession` | `session` | `POST /api/v1/session` | Authenticated |
48+
| `GetProfile` | `profile` | `GET /api/v1/profile` | Authenticated |
49+
| `ValidateJwtToken` | `validate_jwt_token` | `POST /api/v1/validate-jwt` | Public/Service |
50+
| `ValidateSession` | `validate_session` | `POST /api/v1/validate-session` | Public/Service |
51+
| `GetPermissions` | `permissions` | `GET /api/v1/permissions` | Authenticated |
52+
53+
### 2.2. OIDC & OAuth2 REST Endpoints
54+
The following endpoints remain as pure HTTP handlers to comply with strict OIDC/OAuth2 protocol requirements (redirects, form-encoding):
55+
- `GET /.well-known/openid-configuration`
56+
- `GET /.well-known/jwks.json`
57+
- `GET /authorize`
58+
- `GET /userinfo`
59+
- `POST /oauth/token`
60+
- `POST /oauth/revoke`
61+
- `POST /oauth/introspect`
62+
- `GET /oauth_login/:oauth_provider`
63+
64+
---
65+
66+
## 3. Documentation & Commenting Standards
67+
68+
To ensure the generated API documentation (Swagger/OpenAPI) and TypeScript clients are well-documented, the following standards MUST be followed in all `.proto` files:
69+
70+
### 3.1. General Principles
71+
- Use `//` for all descriptions.
72+
- Every RPC, Message, and Field must have a description.
73+
- Start with a clear summary line, followed by details on constraints or behavior.
74+
75+
### 3.2. Field Metadata Labels
76+
Include explicit labels in field comments to denote behavior:
77+
- `// Required.` - For fields that must be provided.
78+
- `// Optional.` - For fields that can be omitted.
79+
- `// Read-only.` - For fields that are only populated by the server in responses.
80+
- `// Output only.` - Similar to read-only, specifically for create/update requests where the field is ignored.
81+
82+
### 3.3. Permission Blocks
83+
Every RPC method comment must include a standardized permission block:
84+
```protobuf
85+
// [Description of the RPC]
86+
//
87+
// Required permissions:
88+
// - [scope_name] (or "Public" for open endpoints)
89+
```
90+
91+
---
92+
93+
## 4. Protocol Buffer Definition Samples
94+
95+
### Permissions, Validations & Documentation
96+
Using the Qdrant pattern for permissions, `protovalidate` for field rules, and the documentation standards defined above.
97+
98+
```protobuf
99+
syntax = "proto3";
100+
101+
package authorizer.v1;
102+
103+
import "google/api/annotations.proto";
104+
import "buf/validate/validate.proto";
105+
106+
// Custom option for granular permission checks
107+
extend google.protobuf.MethodOptions {
108+
string permissions = 50001;
109+
}
110+
111+
service AuthorizerService {
112+
// Signup registers a new user in the system.
113+
//
114+
// Required permissions:
115+
// - Public
116+
rpc Signup(SignUpRequest) returns (AuthResponse) {
117+
option (google.api.http) = {
118+
post: "/api/v1/signup"
119+
body: "*"
120+
};
121+
option (authorizer.v1.permissions) = "";
122+
}
123+
124+
// GetProfile returns the profile of the currently authenticated user.
125+
//
126+
// Required permissions:
127+
// - read:profile
128+
rpc GetProfile(GetProfileRequest) returns (User) {
129+
option (google.api.http) = {
130+
get: "/api/v1/profile"
131+
};
132+
option (authorizer.v1.permissions) = "read:profile";
133+
}
134+
}
135+
136+
// SignUpRequest defines the parameters for user registration.
137+
// It supports both email-based and phone-based signup.
138+
message SignUpRequest {
139+
// The unique email address for the user.
140+
// Optional. If provided, must be a valid email format.
141+
optional string email = 1 [(buf.validate.field).string.email = true];
142+
143+
// The password for the account. Must be at least 8 characters.
144+
// Required.
145+
string password = 2 [(buf.validate.field).string.min_len = 8];
146+
147+
// Confirmation of the password. Must match the 'password' field.
148+
// Required.
149+
string confirm_password = 3 [(buf.validate.field).string.min_len = 8];
150+
151+
// The user's phone number in E.164 format.
152+
// Optional. Example: +1234567890.
153+
optional string phone_number = 4 [(buf.validate.field).string.pattern = "^\\+[1-9]\\d{1,14}$"];
154+
155+
// The first name of the user.
156+
// Optional.
157+
optional string given_name = 5;
158+
159+
// The last name of the user.
160+
// Optional.
161+
optional string family_name = 6;
162+
163+
// List of roles to be assigned to the user.
164+
// Optional. Defaults to project's default roles if empty.
165+
repeated string roles = 7;
166+
167+
// Arbitrary JSON data associated with the user.
168+
// Optional.
169+
map<string, string> app_data = 8;
170+
}
171+
```
172+
173+
---
174+
175+
## 5. Migration Strategy: Interface Pattern
176+
177+
To avoid duplicating business logic between GraphQL and gRPC, all logic is moved to a unified `service.Provider` interface.
178+
179+
### 1. Define the Interface
180+
```go
181+
// internal/service/provider.go
182+
package service
183+
184+
type Provider interface {
185+
// Authentication & Profile
186+
Signup(ctx context.Context, params *SignUpRequest) (*AuthResponse, error)
187+
Login(ctx context.Context, params *LoginRequest) (*AuthResponse, error)
188+
MagicLinkLogin(ctx context.Context, params *MagicLinkLoginRequest) (*Response, error)
189+
Logout(ctx context.Context) (*Response, error)
190+
UpdateProfile(ctx context.Context, params *UpdateProfileRequest) (*Response, error)
191+
VerifyEmail(ctx context.Context, params *VerifyEmailRequest) (*AuthResponse, error)
192+
ResendVerifyEmail(ctx context.Context, params *ResendVerifyEmailRequest) (*Response, error)
193+
ForgotPassword(ctx context.Context, params *ForgotPasswordRequest) (*ForgotPasswordResponse, error)
194+
ResetPassword(ctx context.Context, params *ResetPasswordRequest) (*Response, error)
195+
Revoke(ctx context.Context, params *OAuthRevokeRequest) (*Response, error)
196+
VerifyOtp(ctx context.Context, params *VerifyOTPRequest) (*AuthResponse, error)
197+
ResendOtp(ctx context.Context, params *ResendOTPRequest) (*Response, error)
198+
DeactivateAccount(ctx context.Context) (*Response, error)
199+
200+
// Metadata & Validation
201+
GetMeta(ctx context.Context) (*Meta, error)
202+
GetSession(ctx context.Context, params *SessionQueryRequest) (*AuthResponse, error)
203+
GetProfile(ctx context.Context) (*User, error)
204+
ValidateJwtToken(ctx context.Context, params *ValidateJWTTokenRequest) (*ValidateJWTTokenResponse, error)
205+
ValidateSession(ctx context.Context, params *ValidateSessionRequest) (*ValidateSessionResponse, error)
206+
GetPermissions(ctx context.Context) ([]*Permission, error)
207+
}
208+
```
209+
210+
### 2. Refactor Resolvers
211+
Existing GraphQL resolvers will be thin wrappers around this service, using encapsulated mapping methods:
212+
```go
213+
// internal/graph/schema.resolvers.go
214+
func (r *mutationResolver) Signup(ctx context.Context, params model.SignUpRequest) (*model.AuthResponse, error) {
215+
// Convert GraphQL model to Service request
216+
req := service.SignUpRequestFromGQL(params)
217+
218+
// Call business logic
219+
res, err := r.ServiceProvider.Signup(ctx, req)
220+
if err != nil {
221+
return nil, err
222+
}
223+
224+
// Convert Service response back to GraphQL model
225+
return res.AsGQL(), nil
226+
}
227+
```
228+
229+
### 3. Implement gRPC Handler
230+
The gRPC server will similarly use the encapsulated mapping logic:
231+
```go
232+
// internal/grpc/handler.go
233+
func (s *Server) Signup(ctx context.Context, req *pb.SignUpRequest) (*pb.AuthResponse, error) {
234+
// Convert gRPC proto to Service request
235+
serviceReq := service.SignUpRequestFromPb(req)
236+
237+
// Call business logic
238+
res, err := s.ServiceProvider.Signup(ctx, serviceReq)
239+
if err != nil {
240+
return nil, err
241+
}
242+
243+
// Convert Service response back to gRPC proto
244+
return res.AsPb(), nil
245+
}
246+
```
247+
248+
---
249+
250+
## 6. Detailed Mapping Table
251+
252+
| gRPC Method | GraphQL Field | REST Gateway Path | Perms | Logic Method |
253+
| :--- | :--- | :--- | :--- | :--- |
254+
| `Signup` | `signup` | `POST /api/v1/signup` | - | `Signup` |
255+
| `Login` | `login` | `POST /api/v1/login` | - | `Login` |
256+
| `MagicLinkLogin` | `magic_link_login` | `POST /api/v1/magic-link` | - | `MagicLinkLogin` |
257+
| `Logout` | `logout` | `POST /api/v1/logout` | `auth` | `Logout` |
258+
| `UpdateProfile` | `update_profile` | `PUT /api/v1/profile` | `auth` | `UpdateProfile` |
259+
| `VerifyEmail` | `verify_email` | `POST /api/v1/verify-email` | - | `VerifyEmail` |
260+
| `ResendVerifyEmail` | `resend_verify_email` | `POST /api/v1/resend-verify` | - | `ResendVerifyEmail` |
261+
| `ForgotPassword` | `forgot_password` | `POST /api/v1/forgot-password` | - | `ForgotPassword` |
262+
| `ResetPassword` | `reset_password` | `POST /api/v1/reset-password` | - | `ResetPassword` |
263+
| `Revoke` | `revoke` | `POST /api/v1/revoke` | `auth` | `Revoke` |
264+
| `VerifyOtp` | `verify_otp` | `POST /api/v1/verify-otp` | - | `VerifyOtp` |
265+
| `ResendOtp` | `resend_otp` | `POST /api/v1/resend-otp` | - | `ResendOtp` |
266+
| `DeactivateAccount`| `deactivate_account` | `DELETE /api/v1/account` | `auth` | `DeactivateAccount`|
267+
| `GetMeta` | `meta` | `GET /api/v1/meta` | - | `GetMeta` |
268+
| `GetSession` | `session` | `POST /api/v1/session` | `auth` | `GetSession` |
269+
| `GetProfile` | `profile` | `GET /api/v1/profile` | `auth` | `GetProfile` |
270+
| `ValidateJwtToken` | `validate_jwt_token` | `POST /api/v1/validate-jwt` | - | `ValidateJwtToken` |
271+
| `ValidateSession` | `validate_session` | `POST /api/v1/validate-session` | - | `ValidateSession` |
272+
| `GetPermissions` | `permissions` | `GET /api/v1/permissions` | `auth` | `GetPermissions` |
273+
274+
---
275+
276+
## 7. Testing Strategy
277+
278+
### 1. Service Logic Tests
279+
Unit tests for the `internal/service` implementation using mock storage and memory providers. These tests ensure business logic correctness regardless of the transport layer.
280+
281+
### 2. Integration Tests (End-to-End)
282+
- **gRPC Integration**: Using `buf` generated client to call a test gRPC server.
283+
- **REST Gateway Integration**: Using standard HTTP clients to call the `/api/v1/...` endpoints.
284+
- **GraphQL Regression**: Ensuring existing GraphQL tests still pass after the refactor.
285+
286+
### 3. Validation Tests
287+
Assert that `protovalidate` rules (e.g., email format) correctly reject invalid requests before they reach the business logic.
288+
289+
---
290+
291+
## 8. Development Workflow
292+
293+
1. **Modify Proto**: Edit files in `proto/authorizer/v1/`.
294+
2. **Generate Code**:
295+
```bash
296+
buf generate
297+
```
298+
3. **Implement Service Logic**: Update `internal/service` if new fields or logic are added.
299+
4. **Update Handlers**: Wire the new proto RPC to the service method.

internal/graph/schema.resolvers.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)