diff --git a/go.mod b/go.mod index a0e81c20..f5c8d863 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.4 toolchain go1.23.6 require ( - github.com/eolinker/ap-account v1.0.15 + github.com/eolinker/ap-account v1.0.16 github.com/eolinker/eosc v0.18.3 github.com/eolinker/go-common v1.1.7 github.com/gabriel-vasile/mimetype v1.4.4 diff --git a/go.sum b/go.sum index 431be974..2ff0d7ee 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eolinker/ap-account v1.0.15 h1:n6DJeL6RHZ8eLlZUcY2U3H4d/GPaA5oelAx3R0E6yL8= -github.com/eolinker/ap-account v1.0.15/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g= +github.com/eolinker/ap-account v1.0.16 h1:v1VvSeQ2AvxAvkYT4n4APqZdWS8d1CbA/1O0LYEyNM4= +github.com/eolinker/ap-account v1.0.16/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g= github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A= github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk= github.com/eolinker/go-common v1.1.7 h1:bi7wDmlCYQGjS3k8Bz/o+Mo9aMJAzmPsBLXWurxPfwk= diff --git a/init.go b/init.go index af7b6ca9..4b0a0c86 100644 --- a/init.go +++ b/init.go @@ -4,6 +4,7 @@ package main import ( _ "github.com/APIParkLab/APIPark/frontend" _ "github.com/APIParkLab/APIPark/gateway/apinto" + _ "github.com/APIParkLab/APIPark/login_driver/feishu" _ "github.com/APIParkLab/APIPark/plugins/core" _ "github.com/APIParkLab/APIPark/plugins/openapi" _ "github.com/APIParkLab/APIPark/plugins/permit" diff --git a/login_driver/feishu/client.go b/login_driver/feishu/client.go new file mode 100644 index 00000000..fce3727e --- /dev/null +++ b/login_driver/feishu/client.go @@ -0,0 +1,56 @@ +package feishu + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +var ( + client = http.Client{ + Timeout: 10 * time.Second, + } +) + +func SendRequest[T any](uri string, method string, header http.Header, query url.Values, body []byte) (*T, error) { + if uri == "" { + return nil, fmt.Errorf("invalid URL") + } + + req, err := http.NewRequest(method, uri, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + if query != nil { + req.URL.RawQuery = query.Encode() + } + + if header != nil { + req.Header = header + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + result := new(T) + err = json.Unmarshal(respBody, result) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status code error: %d, response: %s", resp.StatusCode, respBody) + } + + return result, nil +} diff --git a/login_driver/feishu/entity.go b/login_driver/feishu/entity.go new file mode 100644 index 00000000..c8a93b04 --- /dev/null +++ b/login_driver/feishu/entity.go @@ -0,0 +1,27 @@ +package feishu + +type UserTokenResponse struct { + Code int `json:"code"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + RefreshTokenExpiresIn int `json:"refresh_token_expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +type UserInfoResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data UserInfo `json:"data"` +} + +type UserInfo struct { + Name string `json:"name"` + OpenID string `json:"open_id"` + UnionId string `json:"union_id"` + Email string `json:"email"` + Mobile string `json:"mobile"` +} diff --git a/login_driver/feishu/feishu.go b/login_driver/feishu/feishu.go new file mode 100644 index 00000000..a5337999 --- /dev/null +++ b/login_driver/feishu/feishu.go @@ -0,0 +1,144 @@ +package feishu + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/eolinker/eosc/common/bean" + + "github.com/eolinker/ap-account/service/user" + + "github.com/eolinker/go-common/utils" + + "github.com/google/uuid" + + "gorm.io/gorm" + + "github.com/eolinker/ap-account/service/account" + + "github.com/eolinker/ap-account/auth_driver" +) + +const ( + name = "feishu" + title = "飞书" + getTokenUri = "https://open.feishu.cn/open-apis/authen/v2/oauth/token" + getUserInfoUri = "https://open.feishu.cn/open-apis/authen/v1/user_info" +) + +var _ auth_driver.IDriver = (*Driver)(nil) + +func init() { + d := &Driver{} + bean.Autowired(&d.accountService) + bean.Autowired(&d.userService) + auth_driver.Register(name, d) +} + +type Driver struct { + accountService account.IAccountService `autowired:""` + userService user.IUserService `autowired:""` +} + +func (d *Driver) FilterConfig(config map[string]string) { + delete(config, "client_secret") +} + +func (d *Driver) Name() string { + return name +} + +func (d *Driver) Title() string { + return title +} + +func (d *Driver) ThirdLogin(ctx context.Context, args map[string]string) (string, error) { + code, ok := args["code"] + if !ok { + return "", fmt.Errorf("missing code parameter") + } + clientId, ok := args["client_id"] + if !ok { + return "", fmt.Errorf("missing client_id parameter") + } + clientSecret, ok := args["client_secret"] + if !ok { + return "", fmt.Errorf("missing client_secret parameter") + } + tokenResp, err := getUserToken(code, clientId, clientSecret) + if err != nil { + return "", err + } + userInfoResp, err := getUserInfo(tokenResp.TokenType, tokenResp.AccessToken) + if err != nil { + return "", err + } + userId := userInfoResp.Data.UnionId + username := userInfoResp.Data.Name + email := userInfoResp.Data.Email + mobile := userInfoResp.Data.Mobile + info, err := d.accountService.GetIdentifier(ctx, name, userId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return "", err + } + uId := uuid.NewString() + err = d.accountService.Save(ctx, name, uId, userId, utils.Md5(fmt.Sprintf("%s%s", uId, userId))) + if err != nil { + return "", err + } + _, err = d.userService.Create(ctx, uId, username, email, mobile, "") + if err != nil { + return "", err + } + return userId, nil + } + _, err = d.userService.Update(ctx, info.Uid, &username, &email, &mobile) + if err != nil { + return "", err + } + + return userId, nil +} + +func getUserToken(code string, clientId string, clientSecret string) (*UserTokenResponse, error) { + headers := http.Header{} + headers.Set("Content-Type", "application/json") + body := url.Values{} + body.Set("grant_type", "authorization_code") + body.Set("code", code) + body.Set("client_id", clientId) + body.Set("client_secret", clientSecret) + resp, err := SendRequest[UserTokenResponse](getTokenUri, http.MethodPost, headers, nil, []byte(body.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to get user token: %w", err) + } + if resp.Code != 0 { + return nil, fmt.Errorf("failed to get user token: %s", resp.ErrorDescription) + } + return resp, nil +} + +func getUserInfo(tokenType string, token string) (*UserInfoResponse, error) { + headers := http.Header{} + headers.Set("Content-Type", "application/json") + switch tokenType { + case "Bearer": + headers.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + } + resp, err := SendRequest[UserInfoResponse](getUserInfoUri, http.MethodGet, headers, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to get user info: %w", err) + } + if resp.Code != 0 { + return nil, fmt.Errorf("failed to get user info: %s", resp.Msg) + } + return resp, nil +} + +func (d *Driver) Delete(ctx context.Context, ids ...string) error { + return d.accountService.OnRemoveUsers(ctx, ids...) +} diff --git a/resources/access/access.yaml b/resources/access/access.yaml index bafec116..4beef30e 100644 --- a/resources/access/access.yaml +++ b/resources/access/access.yaml @@ -131,6 +131,16 @@ system: value: 'manager' dependents: - system.settings.ai_api.view + - name: login + value: 'login' + children: + - name: view + value: 'view' + guest_allow: true + - name: manager + value: 'manager' + dependents: + - system.settings.login.view - name: ai log value: 'ai_log' children: diff --git a/resources/access/role.yaml b/resources/access/role.yaml index 6637cbae..30714d52 100644 --- a/resources/access/role.yaml +++ b/resources/access/role.yaml @@ -26,6 +26,8 @@ system: - system.settings.general.view - system.settings.log_configuration.manager - system.settings.log_configuration.view + - system.settings.login.manager + - system.settings.login.view - system.settings.mcp.view - system.settings.mcp.manager - system.settings.role.view @@ -69,6 +71,8 @@ system: - system.settings.general.view - system.settings.log_configuration.manager - system.settings.log_configuration.view + - system.settings.login.manager + - system.settings.login.view - system.settings.ssl_certificate.manager - system.settings.ssl_certificate.view - system.settings.strategy.view