diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bb75fe1..4e94130 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,9 +2,11 @@ name: Go on: push: - branches: [ master, main ] + branches: + - main pull_request: - branches: [ master, main ] + branches: + - main jobs: build: diff --git a/README.md b/README.md index 90075cb..42cdde6 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,96 @@ response: {"message":"pong"} ``` +### getToken ### + +The `getToken` API endpoint allows you to generate tokens for different functionalities of the application. This section provides guidelines on how to use the `getToken` endpoint using HTTP POST requests. + +### Endpoint URL + +``` +POST /getToken +``` + +### Request Body + +The request body should contain a JSON payload with the required parameters for generating the tokens. + +The following are the supported token types along with their required parameters: + +1. **RTC Token:** + + To generate an RTC token for video conferencing, include the following parameters in the request body: + + ```json + { + "tokenType": "rtc", + "channel": "your-channel-name", + "role": "publisher", // "publisher" or "subscriber" + "uid": "your-uid", + "expire": 3600 // optional: expiration time in seconds (default: 3600) + } + ``` + +2. **RTM Token:** + + To generate an RTM token for Real-Time Messaging, include the following parameters in the request body: + + ```json + { + "tokenType": "rtm", + "uid": "your-uid", + "expire": 3600 // optional: expiration time in seconds (default: 3600) + } + ``` + +3. **Chat Token:** + + To generate a chat token, include the following parameters in the request body: + + ```json + { + "tokenType": "chat", + "uid": "your-uid", // optional: for generating a user-specific chat token + "expire": 3600 // optional: expiration time in seconds (default: 3600) + } + ``` + +### Response + +Upon successful generation of the token, the API will respond with an HTTP status code of `200 OK`, and the response body will contain the token in a JSON key `"token"`. + +If there is an error during token generation or if the request parameters are invalid, the API will respond with an appropriate HTTP status code and an error message in the response body. + +### Sample Usage + +Here's an example of how to use the `getToken` API endpoint with a POST request using cURL: + +#### Request: + +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "tokenType": "rtc", + "channel": "my-video-channel", + "role": "publisher", + "uid": "user123", + "expire": 3600 +}' "https://your-api-domain.com/getToken" +``` + +#### Reponse: + +```json +{ + "token": "007hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsInN1YiI6InVzZXIxMjMiLCJpYXQiOjE2MzEwNTU4NzIsImV4cCI6MTYzMTA1OTQ3Mn0.3eJ-RGwIl2ANFbdv4SeHtWzGiv6PpC3i0UqXlHfsqEw" +} +``` + +--- + +## Deprecated Methods +The following methods are deprecated but still operational. While they continue to work for backward compatibility, it is advised to refrain from using them in new implementations due to potential future removal or replacement with more efficient alternatives. + + ### RTC Token ### The `rtc` token endpoint requires a `tokenType` (uid || userAccount), `channelName`, and the user's `uid` (type varies based on `tokenType`). `expiry(optional)` Pass an integer to represent the token lifetime in seconds. diff --git a/go.mod b/go.mod index fcc4574..57af3b5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AgoraIO-Community/agora-token-service go 1.19 require ( - github.com/AgoraIO-Community/go-tokenbuilder v1.2.0 + github.com/AgoraIO-Community/go-tokenbuilder v1.3.0 github.com/gin-gonic/gin v1.9.1 github.com/joho/godotenv v1.3.0 ) diff --git a/go.sum b/go.sum index 06aaab5..9cacbe2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/AgoraIO-Community/go-tokenbuilder v1.2.0 h1:Ktlv7n8PhmSap3tgNxjNHO8hj/qydUIj4UOFF8tRxos= -github.com/AgoraIO-Community/go-tokenbuilder v1.2.0/go.mod h1:xqPdaiFG00M1hNN/CCYh8j+NTmkiJsQtqYdf4YAlncA= +github.com/AgoraIO-Community/go-tokenbuilder v1.3.0 h1:x/r/9UnmG9AnWGTH7TkEgbvZJKt2/phl50trw4WP4C4= +github.com/AgoraIO-Community/go-tokenbuilder v1.3.0/go.mod h1:xqPdaiFG00M1hNN/CCYh8j+NTmkiJsQtqYdf4YAlncA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/service/endpoints_test.go b/service/endpoints_test.go index 44cf5da..fa17850 100644 --- a/service/endpoints_test.go +++ b/service/endpoints_test.go @@ -1,6 +1,8 @@ package service import ( + "bytes" + "log" "net/http" "net/http/httptest" "testing" @@ -8,13 +10,41 @@ import ( "github.com/stretchr/testify/assert" ) +func TestGetTokenValidAndInvalid(t *testing.T) { + + tests := []UrlCodePair{ + {"/getToken", http.StatusOK, []byte(`{"tokenType": "rtc", "channel": "channel123", "role": "publisher", "uid": "user123", "expire": 3600}`)}, + {"/getToken", http.StatusOK, []byte(`{"tokenType": "rtm", "uid": "user456", "expire": 1800}`)}, + {"/getToken", http.StatusOK, []byte(`{"tokenType": "chat", "uid": "user789", "expire": 900}`)}, + {"/getToken", http.StatusBadRequest, []byte(`{"channel": "channel456", "role": "subscriber", "expire": 1800}`)}, + {"/getToken", http.StatusBadRequest, []byte(`{"tokenType": "invalid_type", "channel": "channel789", "role": "publisher", "uid": "user123", "expire": 3600}`)}, + {"/getToken", http.StatusBadRequest, []byte(`{"tokenType": "rtc", "role": "publisher", "uid": "user123", "expire": 3600}`)}, + {"/getToken", http.StatusBadRequest, []byte(`{"tokenType": "rtm", "expire": 1800}`)}, + {"/getToken", http.StatusOK, []byte(`{"tokenType": "chat"}`)}, + {"/getToken", http.StatusOK, []byte(`{"tokenType": "chat", "uid": "user123"}`)}, + } + for _, httpTest := range tests { + testApi, err := http.NewRequest(http.MethodPost, httpTest.url, bytes.NewBuffer(httpTest.body)) + log.Println(bytes.NewBuffer(httpTest.body)) + if err != nil { + t.Fatal(err) + } + resp := httptest.NewRecorder() + // Call the endpoint + testService.Server.Handler.ServeHTTP(resp, testApi) + log.Println(httpTest.code) + log.Println(resp.Code) + assert.Equal(t, httpTest.code, resp.Code, resp.Body) + } +} + func TestRtcValidAndInvalid(t *testing.T) { tests := []UrlCodePair{ - {"/rtc/fsda/publisher/uid/0/?expiry=600", http.StatusOK}, - {"/rtc/fsda/publisher/uid//?expiry=600", http.StatusOK}, - {"/rtc/fsda/publisher/uid/test/?expiry=600", http.StatusBadRequest}, - {"/rtc/fsda/publisher/uid/0/?expiry=failing", http.StatusBadRequest}, + {"/rtc/fsda/publisher/uid/0/?expiry=600", http.StatusOK, nil}, + {"/rtc/fsda/publisher/uid//?expiry=600", http.StatusOK, nil}, + {"/rtc/fsda/publisher/uid/test/?expiry=600", http.StatusBadRequest, nil}, + {"/rtc/fsda/publisher/uid/0/?expiry=failing", http.StatusBadRequest, nil}, } for _, httpTest := range tests { testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil) @@ -58,17 +88,18 @@ func TestRtmValidAndInvalid(t *testing.T) { type UrlCodePair struct { url string code int + body []byte } func TestChatValidAndInvalid(t *testing.T) { tests := []UrlCodePair{ - {"/chat/app/", http.StatusOK}, - {"/chat/account/username/", http.StatusOK}, - {"/chat/account/", http.StatusNotFound}, - {"/chat/invalid/", http.StatusNotFound}, - {"/chat/account/username/?expiry=600", http.StatusOK}, - {"/chat/account/username/?expiry=fail", http.StatusBadRequest}, + {"/chat/app/", http.StatusOK, nil}, + {"/chat/account/username/", http.StatusOK, nil}, + {"/chat/account/", http.StatusNotFound, nil}, + {"/chat/invalid/", http.StatusNotFound, nil}, + {"/chat/account/username/?expiry=600", http.StatusOK, nil}, + {"/chat/account/username/?expiry=fail", http.StatusBadRequest, nil}, } for _, httpTest := range tests { testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil) @@ -85,12 +116,12 @@ func TestChatValidAndInvalid(t *testing.T) { func TestRteValidAndInvalid(t *testing.T) { tests := []UrlCodePair{ - {"/rte/channelName/publisher/uid/0/rtmid/?expiry=600", http.StatusOK}, - {"/rte/channelName/publisher/uid/2345/?expiry=600", http.StatusOK}, - {"/rte/channelName/subscriber/uid/0/rtmid/?expiry=600", http.StatusOK}, - {"/rte/channelName/subscriber/uid/2345/?expiry=600", http.StatusOK}, - {"/rte/channelName/publisher/uid/0/?expiry=600", http.StatusBadRequest}, - {"/rte/channelName/publisher/uid/2345/?expiry=failing", http.StatusBadRequest}, + {"/rte/channelName/publisher/uid/0/rtmid/?expiry=600", http.StatusOK, nil}, + {"/rte/channelName/publisher/uid/2345/?expiry=600", http.StatusOK, nil}, + {"/rte/channelName/subscriber/uid/0/rtmid/?expiry=600", http.StatusOK, nil}, + {"/rte/channelName/subscriber/uid/2345/?expiry=600", http.StatusOK, nil}, + {"/rte/channelName/publisher/uid/0/?expiry=600", http.StatusBadRequest, nil}, + {"/rte/channelName/publisher/uid/2345/?expiry=failing", http.StatusBadRequest, nil}, } for _, httpTest := range tests { testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil) diff --git a/service/http_handlers.go b/service/http_handlers.go index 9e9f384..98c3525 100644 --- a/service/http_handlers.go +++ b/service/http_handlers.go @@ -12,7 +12,7 @@ import ( func (s *Service) getRtcToken(c *gin.Context) { log.Println("Generating RTC token") // get param values - channelName, tokenType, uidStr, _, role, expireTimestamp, err := s.parseRtcParams(c) + channelName, tokenType, uidStr, _, role, expire, err := s.parseRtcParams(c) if err != nil { c.Error(err) @@ -23,7 +23,7 @@ func (s *Service) getRtcToken(c *gin.Context) { return } - rtcToken, tokenErr := s.generateRtcToken(channelName, uidStr, tokenType, role, expireTimestamp) + rtcToken, tokenErr := s.generateRtcToken(channelName, uidStr, tokenType, role, expire) if tokenErr != nil { log.Println(tokenErr) // token failed to generate @@ -44,7 +44,7 @@ func (s *Service) getRtcToken(c *gin.Context) { func (s *Service) getRtmToken(c *gin.Context) { log.Println("Generating RTM token") // get param values - uidStr, expireTimestamp, err := s.parseRtmParams(c) + uidStr, expire, err := s.parseRtmParams(c) if err != nil { c.Error(err) @@ -55,7 +55,7 @@ func (s *Service) getRtmToken(c *gin.Context) { return } - rtmToken, tokenErr := rtmtokenbuilder2.BuildToken(s.appID, s.appCertificate, uidStr, expireTimestamp) + rtmToken, tokenErr := rtmtokenbuilder2.BuildToken(s.appID, s.appCertificate, uidStr, expire, "") if tokenErr != nil { c.Error(tokenErr) @@ -105,7 +105,7 @@ func (s *Service) getChatToken(c *gin.Context) { func (s *Service) getRtcRtmToken(c *gin.Context) { log.Println("Generating RTC and RTM tokens") // get rtc param values - channelName, tokenType, uidStr, rtmuid, role, expireTimestamp, rtcParamErr := s.parseRtcParams(c) + channelName, tokenType, uidStr, rtmuid, role, expire, rtcParamErr := s.parseRtcParams(c) if rtcParamErr == nil && rtmuid == "" { rtcParamErr = fmt.Errorf("failed to parse rtm user ID. Cannot be empty or \"0\"") @@ -113,15 +113,15 @@ func (s *Service) getRtcRtmToken(c *gin.Context) { if rtcParamErr != nil { c.Error(rtcParamErr) c.AbortWithStatusJSON(400, gin.H{ - "message": "Error Generating RTC token: " + rtcParamErr.Error(), + "message": "Error Generating RTC and RTM token: " + rtcParamErr.Error(), "status": 400, }) return } // generate the rtcToken - rtcToken, rtcTokenErr := s.generateRtcToken(channelName, uidStr, tokenType, role, expireTimestamp) + rtcToken, rtcTokenErr := s.generateRtcToken(channelName, uidStr, tokenType, role, expire) // generate rtmToken - rtmToken, rtmTokenErr := rtmtokenbuilder2.BuildToken(s.appID, s.appCertificate, rtmuid, expireTimestamp) + rtmToken, rtmTokenErr := rtmtokenbuilder2.BuildToken(s.appID, s.appCertificate, rtmuid, expire, channelName) if rtcTokenErr != nil { c.Error(rtcTokenErr) @@ -132,13 +132,13 @@ func (s *Service) getRtcRtmToken(c *gin.Context) { }) } else if rtmTokenErr != nil { c.Error(rtmTokenErr) - errMsg := "Error Generating RTC token - " + rtmTokenErr.Error() + errMsg := "Error Generating RTM token - " + rtmTokenErr.Error() c.AbortWithStatusJSON(400, gin.H{ "status": 400, "error": errMsg, }) } else { - log.Println("RTC Token generated") + log.Println("RTC and RTM Tokens generated") c.JSON(200, gin.H{ "rtcToken": rtcToken, "rtmToken": rtmToken, diff --git a/service/http_handlers_POST.go b/service/http_handlers_POST.go new file mode 100644 index 0000000..c6cbe43 --- /dev/null +++ b/service/http_handlers_POST.go @@ -0,0 +1,239 @@ +package service + +import ( + "errors" + "net/http" + "strconv" + + "github.com/AgoraIO-Community/go-tokenbuilder/chatTokenBuilder" + rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder" + rtmtokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtmtokenbuilder" + "github.com/gin-gonic/gin" +) + +// getToken handles the HTTP request to generate a token based on the provided tokenType. +// It checks the tokenType from the query parameters and calls the appropriate token generation method. +// The generated token is sent as a JSON response to the client. +// +// Parameters: +// - c: *gin.Context - The Gin context representing the HTTP request and response. +// +// Behavior: +// 1. Retrieves the tokenType from the query parameters, defaulting to "rtc" if not provided. +// 2. Uses a switch statement to handle different tokenType cases: +// - "rtm": Calls the RtmToken method to generate the RTM token and sends it as a JSON response. +// - "chat": Calls the ChatToken method to generate the chat token and sends it as a JSON response. +// - Default: Calls the RtcToken method to generate the RTC token and sends it as a JSON response. +// +// Notes: +// - The actual token generation methods (RtmToken, ChatToken, and RtcToken) are part of the Service struct. +// - The generated token is sent as a JSON response with appropriate HTTP status codes. +// +// Example usage: +// +// router.GET("/getToken", service.getToken) +func (s *Service) getToken(c *gin.Context) { + // Parse the request body into a TokenRequest struct + var tokenReq TokenRequest + if err := c.ShouldBindJSON(&tokenReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var token string + var tokenErr error + + switch tokenReq.TokenType { + case "rtc": + token, tokenErr = s.genRtcToken(tokenReq) + case "rtm": + token, tokenErr = s.genRtmToken(tokenReq) + case "chat": + token, tokenErr = s.genChatToken(tokenReq) + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported tokenType"}) + return + } + if tokenErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": tokenErr.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{ + "token": token, + }) +} + +// TokenRequest is a struct representing the JSON payload structure for token generation requests. +// It contains fields necessary for generating different types of tokens (RTC, RTM, or chat) based on the "TokenType". +// The "Channel", "RtcRole", "Uid", and "ExpirationSeconds" fields are used for specific token types. +// +// TokenType options: "rtc" for RTC token, "rtm" for RTM token, and "chat" for chat token. +type TokenRequest struct { + TokenType string `json:"tokenType"` // The token type: "rtc", "rtm", or "chat" + Channel string `json:"channel,omitempty"` // The channel name (used for RTC and RTM tokens) + RtcRole string `json:"role,omitempty"` // The role of the user for RTC tokens (publisher or subscriber) + Uid string `json:"uid,omitempty"` // The user ID or account (used for RTC, RTM, and some chat tokens) + ExpirationSeconds int `json:"expire,omitempty"` // The token expiration time in seconds (used for all token types) +} + +// genRtcToken generates an RTC token based on the provided TokenRequest and returns it. +// +// Parameters: +// - tokenRequest: TokenRequest - The TokenRequest struct containing the required information for RTC token generation. +// +// Returns: +// - string: The generated RTC token. +// - error: An error if there are any issues during token generation or validation. +// +// Behavior: +// 1. Validates the required fields in the TokenRequest (channel and UID). +// 2. Sets a default expiration time of 3600 seconds (1 hour) if not provided in the request. +// 3. Determines the user's role (publisher or subscriber) based on the "Role" field in the request. +// 4. Generates the RTC token using the rtctokenbuilder2 package. +// +// Notes: +// - The rtctokenbuilder2 package is used for generating RTC tokens. +// - The "Role" field can be "publisher" or "subscriber"; other values are considered invalid. +// +// Example usage: +// +// tokenReq := TokenRequest{ +// TokenType: "rtc", +// Channel: "my_channel", +// Uid: "user123", +// Role: "publisher", +// ExpirationSeconds: 3600, +// } +// token, err := service.genRtcToken(tokenReq) +func (s *Service) genRtcToken(tokenRequest TokenRequest) (string, error) { + if tokenRequest.Channel == "" { + return "", errors.New("invalid: missing channel name") + } + if tokenRequest.Uid == "" { + return "", errors.New("invalid: missing user ID or account") + } + + var userRole rtctokenbuilder2.Role + if tokenRequest.RtcRole == "publisher" { + userRole = rtctokenbuilder2.RolePublisher + } else { + userRole = rtctokenbuilder2.RoleSubscriber + } + + if tokenRequest.ExpirationSeconds == 0 { + tokenRequest.ExpirationSeconds = 3600 + } + + uid64, parseErr := strconv.ParseUint(tokenRequest.Uid, 10, 64) + if parseErr != nil { + return rtctokenbuilder2.BuildTokenWithAccount( + s.appID, s.appCertificate, tokenRequest.Channel, + tokenRequest.Uid, userRole, uint32(tokenRequest.ExpirationSeconds), + ) + } + + return rtctokenbuilder2.BuildTokenWithUid( + s.appID, s.appCertificate, tokenRequest.Channel, + uint32(uid64), userRole, uint32(tokenRequest.ExpirationSeconds), + ) +} + +// genRtmToken generates an RTM (Real-Time Messaging) token based on the provided TokenRequest and returns it. +// +// Parameters: +// - tokenRequest: TokenRequest - The TokenRequest struct containing the required information for RTM token generation. +// +// Returns: +// - string: The generated RTM token. +// - error: An error if there are any issues during token generation or validation. +// +// Behavior: +// 1. Validates the required field in the TokenRequest (UID). +// 2. Sets a default expiration time of 3600 seconds (1 hour) if not provided in the request. +// 3. Generates the RTM token using the rtmtokenbuilder2 package. +// +// Notes: +// - The rtmtokenbuilder2 package is used for generating RTM tokens. +// - The "UID" field in TokenRequest is mandatory for RTM token generation. +// +// Example usage: +// +// tokenReq := TokenRequest{ +// TokenType: "rtm", +// Uid: "user123", +// ExpirationSeconds: 3600, +// } +// token, err := service.genRtmToken(tokenReq) +func (s *Service) genRtmToken(tokenRequest TokenRequest) (string, error) { + if tokenRequest.Uid == "" { + return "", errors.New("invalid: missing user ID or account") + } + if tokenRequest.ExpirationSeconds == 0 { + tokenRequest.ExpirationSeconds = 3600 + } + + return rtmtokenbuilder2.BuildToken( + s.appID, s.appCertificate, + tokenRequest.Uid, + uint32(tokenRequest.ExpirationSeconds), + tokenRequest.Channel, + ) +} + +// genChatToken generates a chat token based on the provided TokenRequest and returns it. +// +// Parameters: +// - tokenRequest: TokenRequest - The TokenRequest struct containing the required information for chat token generation. +// +// Returns: +// - string: The generated chat token. +// - error: An error if there are any issues during token generation or validation. +// +// Behavior: +// 1. Sets a default expiration time of 3600 seconds (1 hour) if not provided in the request. +// 2. Determines whether to generate a chat app token or a chat user token based on the "UID" field in the request. +// 3. Generates the chat token using the chatTokenBuilder package. +// +// Notes: +// - The chatTokenBuilder package is used for generating chat tokens. +// - If the "UID" field is empty, a chat app token is generated; otherwise, a chat user token is generated. +// +// Example usage: +// +// // Generate a chat app token +// tokenReq := TokenRequest{ +// TokenType: "chat", +// ExpirationSeconds: 3600, +// } +// token, err := service.genChatToken(tokenReq) +// +// // Generate a chat user token +// tokenReq := TokenRequest{ +// TokenType: "chat", +// Uid: "user123", +// ExpirationSeconds: 3600, +// } +// token, err := service.genChatToken(tokenReq) +func (s *Service) genChatToken(tokenRequest TokenRequest) (string, error) { + if tokenRequest.ExpirationSeconds == 0 { + tokenRequest.ExpirationSeconds = 3600 + } + + var chatToken string + var tokenErr error + + if tokenRequest.Uid == "" { + chatToken, tokenErr = chatTokenBuilder.BuildChatAppToken( + s.appID, s.appCertificate, uint32(tokenRequest.ExpirationSeconds), + ) + } else { + chatToken, tokenErr = chatTokenBuilder.BuildChatUserToken( + s.appID, s.appCertificate, + tokenRequest.Uid, + uint32(tokenRequest.ExpirationSeconds), + ) + + } + + return chatToken, tokenErr +} diff --git a/service/http_handlers_POST_test.go b/service/http_handlers_POST_test.go new file mode 100644 index 0000000..e527310 --- /dev/null +++ b/service/http_handlers_POST_test.go @@ -0,0 +1,164 @@ +package service + +// Add these imports if they are not already present +import ( + "os" + "testing" +) + +func CreateTestService(t *testing.T) *Service { + appIdEnv, appIDExists := os.LookupEnv("APP_ID") + appCertEnv, appCertExists := os.LookupEnv("APP_CERTIFICATE") + if !appIDExists || !appCertExists { + t.Errorf("check appId or appCertificate") + } + return &Service{ + appID: appIdEnv, + appCertificate: appCertEnv, + } +} + +// TestGenRtcToken tests the genRtcToken function. +func TestGenRtcToken(t *testing.T) { + service := CreateTestService(t) + + // Test valid RTC token generation + tokenReq := TokenRequest{ + TokenType: "rtc", + Channel: "my_channel", + Uid: "user123", + RtcRole: "publisher", + ExpirationSeconds: 3600, + } + + token, err := service.genRtcToken(tokenReq) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + // Add assertions for the expected token value + // For example: assert token is not empty, or has a specific format, etc. + if token == "" { + t.Error("Expected a non-empty RTC token") + } + + // Test missing expiration, subscriber, int Uid (optional param) + validWithoutExpiration := TokenRequest{ + TokenType: "rtc", + Channel: "my_channel", + Uid: "123", + RtcRole: "subscriber", + } + + _, err = service.genRtcToken(validWithoutExpiration) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Test invalid RTC token generation (missing channel name) + invalidTokenReq := TokenRequest{ + TokenType: "rtc", + RtcRole: "publisher", + ExpirationSeconds: 3600, + } + + _, err = service.genRtcToken(invalidTokenReq) + if err == nil { + t.Error("Expected error, but got nil") + } + + // Test invalid RTC token generation (missing channel name) + invalidTokenReq2 := TokenRequest{ + TokenType: "rtc", + Channel: "my_channel", + RtcRole: "subscriber", + } + + _, err = service.genRtcToken(invalidTokenReq2) + if err == nil { + t.Error("Expected error, but got nil") + } +} + +// TestGenRtmToken tests the genRtmToken function. +func TestGenRtmToken(t *testing.T) { + service := CreateTestService(t) + + // Test valid RTM token generation + tokenReq := TokenRequest{ + TokenType: "rtm", + Uid: "user123", + ExpirationSeconds: 3600, + } + + token, err := service.genRtmToken(tokenReq) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + // Add assertions for the expected token value + // For example: assert token is not empty, or has a specific format, etc. + if token == "" { + t.Error("Expected a non-empty RTM token") + } + + // Test invalid RTM token generation (missing user ID) + invalidTokenReq := TokenRequest{ + TokenType: "rtm", + ExpirationSeconds: 3600, + } + + _, err = service.genRtmToken(invalidTokenReq) + if err == nil { + t.Error("Expected error, but got nil") + } + // Add assertions for the expected error message, for example: + // assert error contains "missing user ID or account" +} + +// TestGenChatToken tests the genChatToken function. +func TestGenChatToken(t *testing.T) { + service := CreateTestService(t) + + // Test valid chat token generation (chat app token) + tokenReqApp := TokenRequest{ + TokenType: "chat", + ExpirationSeconds: 3600, + } + + tokenApp, err := service.genChatToken(tokenReqApp) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + // Add assertions for the expected tokenApp value + // For example: assert tokenApp is not empty, or has a specific format, etc. + if tokenApp == "" { + t.Error("Expected a non-empty chat app token") + } + + // Test valid chat token generation (chat user token) + tokenReqUser := TokenRequest{ + TokenType: "chat", + Uid: "user123", + ExpirationSeconds: 3600, + } + + tokenUser, err := service.genChatToken(tokenReqUser) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + // Add assertions for the expected tokenUser value + // For example: assert tokenUser is not empty, or has a specific format, etc. + if tokenUser == "" { + t.Error("Expected a non-empty chat user token") + } + + // Test chat token generation without expiration + invalidTokenReq := TokenRequest{ + TokenType: "chat", + Uid: "user123", + } + + _, err = service.genChatToken(invalidTokenReq) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} diff --git a/service/parsing.go b/service/parsing.go index 843a4d9..1a9e9bb 100644 --- a/service/parsing.go +++ b/service/parsing.go @@ -4,13 +4,44 @@ import ( "fmt" "strconv" "strings" - "time" rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder" "github.com/gin-gonic/gin" ) -func (s *Service) parseRtcParams(c *gin.Context) (channelName, tokenType, uidStr string, rtmuid string, role rtctokenbuilder2.Role, expireTimestamp uint32, err error) { +// parseRtcParams extracts and parses the required parameters from the given Gin context (HTTP request). +// It retrieves various parameters such as channelName, tokenType, rtcuid, rtmuid, role, and expiry, +// and performs necessary conversions and validations. +// +// Parameters: +// +// c: *gin.Context - The Gin context representing the HTTP request and response. +// +// Returns: +// - channelName: string - The name of the video conferencing channel. +// - tokenType: string - The type of RTC token. +// - uidStr: string - The user ID for the RTC token. +// - rtmuid: string - The user ID for the RTM (Real-Time Messaging) token. +// - role: rtctokenbuilder2.Role - The role of the user (Publisher or Subscriber) in the video conferencing. +// - expire: uint32 - The expiration time of the token in seconds. +// - err: error - Any error that occurred during parameter parsing. Nil if parsing was successful. +// +// Behavior: +// 1. Retrieves the values of channelName, roleStr, tokenType, rtcuid, and rtmuid from the Gin context. +// 2. Sets uidStr to "0" if it is empty, implying that any user ID is allowed. +// 3. If rtmuid is empty and uidStr is not "0", it sets rtmuid to uidStr. +// 4. Determines the role based on the value of roleStr. "publisher" maps to RolePublisher, else RoleSubscriber. +// 5. Parses the expiry time from the query parameter "expiry" and converts it to uint32. +// 6. If string conversion fails for the expiry time, sets err to an error with the failure information. +// +// Notes: +// - The `rtctokenbuilder2.Role` type represents the role of a user in the video conferencing token. +// It might be an enumeration or constant representing different roles (e.g., Publisher and Subscriber). +// +// Example usage: +// +// channelName, tokenType, uidStr, rtmuid, role, expire, err := parseRtcParams(context) +func (s *Service) parseRtcParams(c *gin.Context) (channelName, tokenType, uidStr string, rtmuid string, role rtctokenbuilder2.Role, expire uint32, err error) { // get param values channelName = c.Param("channelName") roleStr := c.Param("role") @@ -40,40 +71,31 @@ func (s *Service) parseRtcParams(c *gin.Context) (channelName, tokenType, uidStr // if string conversion fails return an error err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) } + expire = uint32(expireTime64) - // set timestamps - expireTimeInSeconds := uint32(expireTime64) - currentTimestamp := uint32(time.Now().UTC().Unix()) - expireTimestamp = currentTimestamp + expireTimeInSeconds - - return channelName, tokenType, uidStr, rtmuid, role, expireTimestamp, err + return channelName, tokenType, uidStr, rtmuid, role, expire, err } -func (s *Service) parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp uint32, err error) { +func (s *Service) parseRtmParams(c *gin.Context) (uidStr string, expire uint32, err error) { // get param values uidStr = c.Param("rtmuid") expireTime := c.DefaultQuery("expiry", "3600") - expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64) if parseErr != nil { // if string conversion fails return an error err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) } + expire = uint32(expireTime64) if uidStr == "" || uidStr == "0" { err = fmt.Errorf("invalid RTM User ID: \"%s\"", uidStr) } - // set timestamps - expireTimeInSeconds := uint32(expireTime64) - currentTimestamp := uint32(time.Now().UTC().Unix()) - expireTimestamp = currentTimestamp + expireTimeInSeconds - // check if string conversion fails - return uidStr, expireTimestamp, err + return uidStr, expire, err } -func (s *Service) parseChatParams(c *gin.Context) (uidStr string, tokenType string, expireTimestamp uint32, err error) { +func (s *Service) parseChatParams(c *gin.Context) (uidStr string, tokenType string, expire uint32, err error) { // get param values uidStr = c.Param("chatid") urlSplit := strings.Split(c.Request.URL.Path, "/") @@ -84,23 +106,20 @@ func (s *Service) parseChatParams(c *gin.Context) (uidStr string, tokenType stri } } expireTime := c.DefaultQuery("expiry", "3600") + expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64) + if parseErr != nil { + // if string conversion fails return an error + err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) + } + expire = uint32(expireTime64) if tokenType == "account" { tokenType = "userAccount" } if uidStr == "" && tokenType != "app" { err = fmt.Errorf("userAccount type requires chat ID") - return uidStr, tokenType, expireTimestamp, err - } - expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64) - if parseErr != nil { - // if string conversion fails return an error - err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) + return uidStr, tokenType, expire, err } - // set timestamps - expireTimeInSeconds := uint32(expireTime64) - currentTimestamp := uint32(time.Now().UTC().Unix()) - expireTimestamp = currentTimestamp + expireTimeInSeconds // check if string conversion fails - return uidStr, tokenType, expireTimestamp, err + return uidStr, tokenType, expire, err } diff --git a/service/service.go b/service/service.go index ec1088d..885d993 100644 --- a/service/service.go +++ b/service/service.go @@ -86,6 +86,7 @@ func NewService() *Service { api.GET("chat/app/", s.getChatToken) // Chat token for API calls api.GET("chat/account/:chatid/", s.getChatToken) // Chat token for SDK calls + api.POST("/getToken", s.getToken) s.Server.Handler = api return s } diff --git a/service/tokens.go b/service/tokens.go index d018f01..1068fe9 100644 --- a/service/tokens.go +++ b/service/tokens.go @@ -9,13 +9,39 @@ import ( rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder" ) -func (s *Service) generateRtcToken(channelName, uidStr, tokenType string, role rtctokenbuilder2.Role, expireTimestamp uint32) (rtcToken string, err error) { +// generateRtcToken generates an RTC token for the video conferencing application based on the provided parameters. +// +// Parameters: +// - channelName: string - The name of the video conferencing channel. +// - uidStr: string - The user ID for the RTC token, represented as a string. +// - tokenType: string - The type of RTC token. Can be "userAccount" or "uid". +// - role: rtctokenbuilder2.Role - The role of the user (Publisher or Subscriber) in the video conferencing. +// - expireDelta: uint32 - The duration of the token's validity in seconds. +// +// Returns: +// - rtcToken: string - The generated RTC token as a string. +// - err: error - Any error that occurred during token generation. Nil if token generation was successful. +// +// Behavior: +// 1. Checks the tokenType to determine whether to build the token using the userAccount or uid. +// 2. If the tokenType is "userAccount", builds the RTC token using the user account (uidStr). +// 3. If the tokenType is "uid", parses uidStr to an unsigned 64-bit integer and converts it to uint32. +// 4. Builds the RTC token using the numeric user ID (uid) and the provided role and expireDelta. +// 5. If the tokenType is neither "userAccount" nor "uid", returns an error indicating the unknown tokenType. +// +// Notes: +// - The `rtctokenbuilder2.Role` type represents the role of a user in the video conferencing token. +// It might be an enumeration or constant representing different roles (e.g., Publisher and Subscriber). +// +// Example usage: +// +// rtcToken, err := generateRtcToken("channel123", "user123", "userAccount", rtctokenbuilder2.RolePublisher, 3600) +func (s *Service) generateRtcToken(channelName, uidStr, tokenType string, role rtctokenbuilder2.Role, expireDelta uint32) (rtcToken string, err error) { if tokenType == "userAccount" { - log.Printf("Building Token with userAccount: %s\n", uidStr) - rtcToken, err = rtctokenbuilder2.BuildTokenWithAccount(s.appID, s.appCertificate, channelName, uidStr, role, expireTimestamp) + log.Printf("Building Token for userAccount: %s\n", uidStr) + rtcToken, err = rtctokenbuilder2.BuildTokenWithAccount(s.appID, s.appCertificate, channelName, uidStr, role, expireDelta) return rtcToken, err - } else if tokenType == "uid" { uid64, parseErr := strconv.ParseUint(uidStr, 10, 64) // check if conversion fails @@ -25,8 +51,8 @@ func (s *Service) generateRtcToken(channelName, uidStr, tokenType string, role r } uid := uint32(uid64) // convert uid from uint64 to uint 32 - log.Printf("Building Token with uid: %d\n", uid) - rtcToken, err = rtctokenbuilder2.BuildTokenWithUid(s.appID, s.appCertificate, channelName, uid, role, expireTimestamp) + log.Printf("Building Token for uid: %d\n", uid) + rtcToken, err = rtctokenbuilder2.BuildTokenWithUid(s.appID, s.appCertificate, channelName, uid, role, expireDelta) return rtcToken, err } else { err = fmt.Errorf("failed to generate RTC token for Unknown Tokentype: %s", tokenType)