feat: add growth tracking page and related functionality
- Implemented a new Growth page to track practice time and trends. - Added API integration for fetching review data. - Created components for displaying practice statistics and trends. - Updated navigation titles for the main index and growth pages. - Removed unused styles from the index page. - Introduced a Projects management page for adding and editing practice projects. - Developed a Record form for logging practice sessions with validation. - Added utility functions for date manipulation and duration formatting. - Implemented error handling and session management in the practice service. - Created unit tests for the practice service to ensure reliability.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidCode = errors.New("WeChat login code is invalid or expired")
|
||||
var errUnavailable = errors.New("WeChat identity verification is unavailable")
|
||||
|
||||
// WeChat exchanges wx.login codes at the server only. Client is an optional
|
||||
// network dependency for tests; production uses a bounded HTTPS client.
|
||||
type WeChat struct {
|
||||
AppID string
|
||||
AppSecret string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Exchange returns an app-scoped openid after WeChat verifies the code. It never
|
||||
// returns session_key or wraps URL errors, since exchange URLs contain secrets.
|
||||
func (w *WeChat) Exchange(ctx context.Context, code string) (string, error) {
|
||||
if w.AppID == "" || w.AppSecret == "" {
|
||||
return "", errUnavailable
|
||||
}
|
||||
query := url.Values{"appid": {w.AppID}, "secret": {w.AppSecret}, "js_code": {code}, "grant_type": {"authorization_code"}}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+query.Encode(), nil)
|
||||
if err != nil {
|
||||
return "", errUnavailable
|
||||
}
|
||||
client := w.Client
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", errUnavailable
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != 200 {
|
||||
return "", errUnavailable
|
||||
}
|
||||
var result struct {
|
||||
OpenID string `json:"openid"`
|
||||
ErrCode int `json:"errcode"`
|
||||
}
|
||||
if err = json.NewDecoder(io.LimitReader(response.Body, 16*1024)).Decode(&result); err != nil {
|
||||
return "", errUnavailable
|
||||
}
|
||||
if result.ErrCode == 40029 || result.ErrCode == 40163 {
|
||||
return "", ErrInvalidCode
|
||||
}
|
||||
if result.ErrCode != 0 || result.OpenID == "" {
|
||||
return "", errUnavailable
|
||||
}
|
||||
return w.AppID + ":" + result.OpenID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user