Compare commits

...

9 Commits

Author SHA1 Message Date
43bebc03b9 feat: fix crash in roleChangeTrigger() 2025-01-09 16:41:56 +08:00
c5f25cbc7d feat: getPidByPort() supports alpine now (#3483)
Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2025-01-08 12:18:46 +08:00
3feb6ce84d feat: add Kwai OAuth provider (#3480)
* feat: add Kwai OAuth provider

* fix: incorrect parameter in getAuthUrl
2025-01-08 00:09:16 +08:00
08d6b45fc5 feat: keeps "build" folder during yarn build 2025-01-07 23:38:50 +08:00
56d0de64dc feat: support StopOldInstance() 2025-01-07 21:39:21 +08:00
1813e8e8c7 feat: return goroutine error in get-dashboard API (#3479) 2025-01-07 10:35:45 +08:00
e27c764a55 feat: fix bug that GitHub oauth provider shows error if failed to fetch user's email (#3474)
* fix: fix github idp will stop login if it cannot fetch user's email through al restful api

* Update github.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2025-01-05 20:25:42 +08:00
e5a2057382 feat: fix empty scope bug in RefreshToken API (#3467)
* fix: fix scope will be empty when user not passing scope in refresh api

* fix: promote code format
2025-01-02 12:53:17 +08:00
8457ff7433 feat: support radiusDefaultOrganization in app.conf 2025-01-02 00:10:58 +08:00
20 changed files with 409 additions and 51 deletions

View File

@ -28,6 +28,7 @@ ldapServerPort = 389
ldapsCertId = "" ldapsCertId = ""
ldapsServerPort = 636 ldapsServerPort = 636
radiusServerPort = 1812 radiusServerPort = 1812
radiusDefaultOrganization = "built-in"
radiusSecret = "secret" radiusSecret = "secret"
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1} quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"} logConfig = {"filename": "logs/casdoor.log", "maxdays":99999, "perm":"0770"}

View File

@ -248,16 +248,17 @@ func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, fmt.Errorf("%s, %s", errMessage.Message, errMessage.DocumentationUrl)
}
var userEmails []GitHubUserEmailInfo fmt.Printf("GithubIdProvider:GetUserInfo() error, status code = %d, error message = %v\n", respEmail.StatusCode, errMessage)
err = json.Unmarshal(emailBody, &userEmails) } else {
if err != nil { var userEmails []GitHubUserEmailInfo
return nil, err err = json.Unmarshal(emailBody, &userEmails)
} if err != nil {
return nil, err
}
githubUserInfo.Email = idp.getEmailFromEmailsResult(userEmails) githubUserInfo.Email = idp.getEmailFromEmailsResult(userEmails)
}
} }
userInfo := UserInfo{ userInfo := UserInfo{

161
idp/kwai.go Normal file
View File

@ -0,0 +1,161 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/oauth2"
)
type KwaiIdProvider struct {
Client *http.Client
Config *oauth2.Config
}
func NewKwaiIdProvider(clientId string, clientSecret string, redirectUrl string) *KwaiIdProvider {
idp := &KwaiIdProvider{}
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
return idp
}
func (idp *KwaiIdProvider) SetHttpClient(client *http.Client) {
idp.Client = client
}
func (idp *KwaiIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
endpoint := oauth2.Endpoint{
TokenURL: "https://open.kuaishou.com/oauth2/access_token",
AuthURL: "https://open.kuaishou.com/oauth2/authorize", // qr code: /oauth2/connect
}
config := &oauth2.Config{
Scopes: []string{"user_info"},
Endpoint: endpoint,
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
}
return config
}
type KwaiTokenResp struct {
Result int `json:"result"`
ErrorMsg string `json:"error_msg"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
OpenId string `json:"open_id"`
Scopes []string `json:"scopes"`
}
// GetToken use code to get access_token
func (idp *KwaiIdProvider) GetToken(code string) (*oauth2.Token, error) {
params := map[string]string{
"app_id": idp.Config.ClientID,
"app_secret": idp.Config.ClientSecret,
"code": code,
"grant_type": "authorization_code",
}
tokenUrl := fmt.Sprintf("%s?app_id=%s&app_secret=%s&code=%s&grant_type=authorization_code",
idp.Config.Endpoint.TokenURL, params["app_id"], params["app_secret"], params["code"])
resp, err := idp.Client.Get(tokenUrl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var tokenResp KwaiTokenResp
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return nil, err
}
if tokenResp.Result != 1 {
return nil, fmt.Errorf("get token error: %s", tokenResp.ErrorMsg)
}
token := &oauth2.Token{
AccessToken: tokenResp.AccessToken,
RefreshToken: tokenResp.RefreshToken,
Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
}
raw := make(map[string]interface{})
raw["open_id"] = tokenResp.OpenId
token = token.WithExtra(raw)
return token, nil
}
// More details: https://open.kuaishou.com/openapi/user_info
type KwaiUserInfo struct {
Result int `json:"result"`
ErrorMsg string `json:"error_msg"`
UserInfo struct {
Head string `json:"head"`
Name string `json:"name"`
Sex string `json:"sex"`
City string `json:"city"`
} `json:"user_info"`
}
// GetUserInfo use token to get user profile
func (idp *KwaiIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
userInfoUrl := fmt.Sprintf("https://open.kuaishou.com/openapi/user_info?app_id=%s&access_token=%s",
idp.Config.ClientID, token.AccessToken)
resp, err := idp.Client.Get(userInfoUrl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var kwaiUserInfo KwaiUserInfo
err = json.Unmarshal(body, &kwaiUserInfo)
if err != nil {
return nil, err
}
if kwaiUserInfo.Result != 1 {
return nil, fmt.Errorf("get user info error: %s", kwaiUserInfo.ErrorMsg)
}
userInfo := &UserInfo{
Id: token.Extra("open_id").(string),
Username: kwaiUserInfo.UserInfo.Name,
DisplayName: kwaiUserInfo.UserInfo.Name,
AvatarUrl: kwaiUserInfo.UserInfo.Head,
Extra: map[string]string{
"gender": kwaiUserInfo.UserInfo.Sex,
"city": kwaiUserInfo.UserInfo.City,
},
}
return userInfo, nil
}

View File

@ -113,6 +113,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil return NewOktaIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "Douyin": case "Douyin":
return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewDouyinIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Kwai":
return NewKwaiIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Bilibili": case "Bilibili":
return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil return NewBilibiliIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "MetaMask": case "MetaMask":

View File

@ -83,6 +83,11 @@ func main() {
// logs.SetLevel(logs.LevelInformational) // logs.SetLevel(logs.LevelInformational)
logs.SetLogFuncCall(false) logs.SetLogFuncCall(false)
err = util.StopOldInstance(port)
if err != nil {
panic(err)
}
go ldap.StartLdapServer() go ldap.StartLdapServer()
go radius.StartRadiusServer() go radius.StartRadiusServer()
go object.ClearThroughputPerSecond() go object.ClearThroughputPerSecond()

View File

@ -41,11 +41,11 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var err error var err error
wg.Add(len(tableNames)) wg.Add(len(tableNames))
ch := make(chan error, len(tableNames))
for _, tableName := range tableNames { for _, tableName := range tableNames {
dashboard[tableName+"Counts"] = make([]int64, 31) dashboard[tableName+"Counts"] = make([]int64, 31)
tableName := tableName tableName := tableName
go func() { go func(ch chan error) {
defer wg.Done() defer wg.Done()
dashboardDateItems := []DashboardDateItem{} dashboardDateItems := []DashboardDateItem{}
var countResult int64 var countResult int64
@ -59,20 +59,29 @@ func GetDashboard(owner string) (*map[string][]int64, error) {
} }
if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil { if countResult, err = dbQueryBefore.And("created_time < ?", time30day).Table(tableName).Count(); err != nil {
panic(err) ch <- err
return
} }
if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil { if err = dbQueryAfter.And("created_time >= ?", time30day).Table(tableName).Find(&dashboardDateItems); err != nil {
panic(err) ch <- err
return
} }
dashboardMap.Store(tableName, DashboardMapItem{ dashboardMap.Store(tableName, DashboardMapItem{
dashboardDateItems: dashboardDateItems, dashboardDateItems: dashboardDateItems,
itemCount: countResult, itemCount: countResult,
}) })
}() }(ch)
} }
wg.Wait() wg.Wait()
close(ch)
for err = range ch {
if err != nil {
return nil, err
}
}
nowTime := time.Now() nowTime := time.Now()
for i := 30; i >= 0; i-- { for i := 30; i >= 0; i-- {

View File

@ -338,6 +338,10 @@ func roleChangeTrigger(oldName string, newName string) error {
for _, role := range roles { for _, role := range roles {
for j, u := range role.Roles { for j, u := range role.Roles {
if u == "*" {
continue
}
owner, name := util.GetOwnerAndNameFromId(u) owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName { if name == oldName {
role.Roles[j] = util.GetId(owner, newName) role.Roles[j] = util.GetId(owner, newName)
@ -358,6 +362,10 @@ func roleChangeTrigger(oldName string, newName string) error {
for _, permission := range permissions { for _, permission := range permissions {
for j, u := range permission.Roles { for j, u := range permission.Roles {
// u = organization/username // u = organization/username
if u == "*" {
continue
}
owner, name := util.GetOwnerAndNameFromId(u) owner, name := util.GetOwnerAndNameFromId(u)
if name == oldName { if name == oldName {
permission.Roles[j] = util.GetId(owner, newName) permission.Roles[j] = util.GetId(owner, newName)

View File

@ -309,22 +309,29 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
}, nil }, nil
} }
var oldTokenScope string
if application.TokenFormat == "JWT-Standard" { if application.TokenFormat == "JWT-Standard" {
_, err = ParseStandardJwtToken(refreshToken, cert) oldToken, err := ParseStandardJwtToken(refreshToken, cert)
if err != nil { if err != nil {
return &TokenError{ return &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()), ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil }, nil
} }
oldTokenScope = oldToken.Scope
} else { } else {
_, err = ParseJwtToken(refreshToken, cert) oldToken, err := ParseJwtToken(refreshToken, cert)
if err != nil { if err != nil {
return &TokenError{ return &TokenError{
Error: InvalidGrant, Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()), ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil }, nil
} }
oldTokenScope = oldToken.Scope
}
if scope == "" {
scope = oldTokenScope
} }
// generate a new token // generate a new token

View File

@ -129,6 +129,7 @@ type User struct {
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"` Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
Okta string `xorm:"okta varchar(100)" json:"okta"` Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin varchar(100)" json:"douyin"` Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
Kwai string `xorm:"kwai varchar(100)" json:"kwai"`
Line string `xorm:"line varchar(100)" json:"line"` Line string `xorm:"line varchar(100)" json:"line"`
Amazon string `xorm:"amazon varchar(100)" json:"amazon"` Amazon string `xorm:"amazon varchar(100)" json:"amazon"`
Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"` Auth0 string `xorm:"auth0 varchar(100)" json:"auth0"`
@ -698,7 +699,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts", "is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled", "signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon", "baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "kwai", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox", "auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup", "eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",

View File

@ -68,8 +68,10 @@ func handleAccessRequest(w radius.ResponseWriter, r *radius.Request) {
log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password) log.Printf("handleAccessRequest() username=%v, org=%v, password=%v", username, organization, password)
if organization == "" { if organization == "" {
w.Write(r.Response(radius.CodeAccessReject)) organization = conf.GetConfigString("radiusDefaultOrganization")
return if organization == "" {
organization = "built-in"
}
} }
var user *object.User var user *object.User

View File

@ -7558,6 +7558,9 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
}, },
"kwai": {
"type": "string"
},
"language": { "language": {
"type": "string" "type": "string"
}, },

View File

@ -4981,6 +4981,8 @@ definitions:
karma: karma:
type: integer type: integer
format: int64 format: int64
kwai:
type: string
language: language:
type: string type: string
lark: lark:

97
util/process.go Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
)
func getPidByPort(port int) (int, error) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "netstat -ano | findstr :"+strconv.Itoa(port))
case "darwin", "linux":
cmd = exec.Command("lsof", "-t", "-i", ":"+strconv.Itoa(port))
default:
return 0, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 1 {
return 0, nil
}
} else {
return 0, err
}
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) > 0 {
if runtime.GOOS == "windows" {
if fields[1] == "0.0.0.0:"+strconv.Itoa(port) {
pid, err := strconv.Atoi(fields[len(fields)-1])
if err != nil {
return 0, err
}
return pid, nil
}
} else {
pid, err := strconv.Atoi(fields[0])
if err != nil {
return 0, err
}
return pid, nil
}
}
}
return 0, nil
}
func StopOldInstance(port int) error {
pid, err := getPidByPort(port)
if err != nil {
return err
}
if pid == 0 {
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
return err
}
err = process.Kill()
if err != nil {
return err
} else {
fmt.Printf("The old instance with pid: %d has been stopped\n", pid)
}
return nil
}

View File

@ -1,4 +1,5 @@
const CracoLessPlugin = require("craco-less"); const CracoLessPlugin = require("craco-less");
const path = require("path");
module.exports = { module.exports = {
devServer: { devServer: {
@ -55,47 +56,42 @@ module.exports = {
}, },
], ],
webpack: { webpack: {
configure: { configure: (webpackConfig, { env, paths }) => {
// ignore webpack warnings by source-map-loader paths.appBuild = path.resolve(__dirname, "build-temp");
webpackConfig.output.path = path.resolve(__dirname, "build-temp");
// ignore webpack warnings by source-map-loader
// https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546 // https://github.com/facebook/create-react-app/pull/11752#issuecomment-1345231546
ignoreWarnings: [ webpackConfig.ignoreWarnings = [
function ignoreSourcemapsloaderWarnings(warning) { function ignoreSourcemapsloaderWarnings(warning) {
return ( return (
warning.module && warning.module &&
warning.module.resource.includes('node_modules') && warning.module.resource.includes("node_modules") &&
warning.details && warning.details &&
warning.details.includes('source-map-loader') warning.details.includes("source-map-loader")
) );
}, },
], ];
// use polyfill Buffer with Webpack 5 // use polyfill Buffer with Webpack 5
// https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5 // https://viglucci.io/articles/how-to-polyfill-buffer-with-webpack-5
// https://craco.js.org/docs/configuration/webpack/ // https://craco.js.org/docs/configuration/webpack/
resolve: { webpackConfig.resolve.fallback = {
fallback: { buffer: require.resolve("buffer/"),
// "process": require.resolve('process/browser'), process: false,
// "util": require.resolve("util/"), util: false,
// "url": require.resolve("url/"), url: false,
// "zlib": require.resolve("browserify-zlib"), zlib: false,
// "stream": require.resolve("stream-browserify"), stream: false,
// "http": require.resolve("stream-http"), http: false,
// "https": require.resolve("https-browserify"), https: false,
// "assert": require.resolve("assert/"), assert: false,
"buffer": require.resolve('buffer/'), crypto: false,
"process": false, os: false,
"util": false, fs: false,
"url": false, };
"zlib": false,
"stream": false, return webpackConfig;
"http": false,
"https": false,
"assert": false,
"buffer": false,
"crypto": false,
"os": false,
"fs": false,
},
}
}, },
} },
}; };

21
web/mv.js Normal file
View File

@ -0,0 +1,21 @@
const fs = require("fs");
const path = require("path");
const sourceDir = path.join(__dirname, "build-temp");
const targetDir = path.join(__dirname, "build");
if (!fs.existsSync(sourceDir)) {
// eslint-disable-next-line no-console
console.error(`Source directory "${sourceDir}" does not exist.`);
process.exit(1);
}
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, {recursive: true, force: true});
// eslint-disable-next-line no-console
console.log(`Target directory "${targetDir}" has been deleted successfully.`);
}
fs.renameSync(sourceDir, targetDir);
// eslint-disable-next-line no-console
console.log(`Renamed "${sourceDir}" to "${targetDir}" successfully.`);

View File

@ -57,6 +57,7 @@
"scripts": { "scripts": {
"start": "cross-env PORT=7001 craco start", "start": "cross-env PORT=7001 craco start",
"build": "craco build", "build": "craco build",
"postbuild": "node mv.js",
"test": "craco test", "test": "craco test",
"eject": "craco eject", "eject": "craco eject",
"crowdin:sync": "crowdin upload && crowdin download", "crowdin:sync": "crowdin upload && crowdin download",

View File

@ -985,6 +985,7 @@ export function getProviderTypeOptions(category) {
{id: "Bilibili", name: "Bilibili"}, {id: "Bilibili", name: "Bilibili"},
{id: "Okta", name: "Okta"}, {id: "Okta", name: "Okta"},
{id: "Douyin", name: "Douyin"}, {id: "Douyin", name: "Douyin"},
{id: "Kwai", name: "Kwai"},
{id: "Line", name: "Line"}, {id: "Line", name: "Line"},
{id: "Amazon", name: "Amazon"}, {id: "Amazon", name: "Amazon"},
{id: "Auth0", name: "Auth0"}, {id: "Auth0", name: "Auth0"},

View File

@ -0,0 +1,31 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {createButton} from "react-social-login-buttons";
import {StaticBaseUrl} from "../Setting";
function Icon({width = 24, height = 24}) {
return <img src={`${StaticBaseUrl}/buttons/kwai.svg`} alt="Sign in with Kwai" style={{width: width, height: height}} />;
}
const config = {
text: "Sign in with Kwai",
icon: Icon,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const KwaiLoginButton = createButton(config);
export default KwaiLoginButton;

View File

@ -119,6 +119,10 @@ const authInfo = {
scope: "user_info", scope: "user_info",
endpoint: "https://open.douyin.com/platform/oauth/connect", endpoint: "https://open.douyin.com/platform/oauth/connect",
}, },
Kwai: {
scope: "user_info",
endpoint: "https://open.kuaishou.com/oauth2/connect",
},
Custom: { Custom: {
endpoint: "https://example.com/", endpoint: "https://example.com/",
}, },
@ -470,6 +474,8 @@ export function getAuthUrl(application, provider, method, code) {
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Douyin" || provider.type === "TikTok") { } else if (provider.type === "Douyin" || provider.type === "TikTok") {
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`; return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Kwai") {
return `${endpoint}?app_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} else if (provider.type === "Custom") { } else if (provider.type === "Custom") {
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`; return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.scopes}&response_type=code&state=${state}`;
} else if (provider.type === "Bilibili") { } else if (provider.type === "Bilibili") {

View File

@ -40,6 +40,7 @@ import SteamLoginButton from "./SteamLoginButton";
import BilibiliLoginButton from "./BilibiliLoginButton"; import BilibiliLoginButton from "./BilibiliLoginButton";
import OktaLoginButton from "./OktaLoginButton"; import OktaLoginButton from "./OktaLoginButton";
import DouyinLoginButton from "./DouyinLoginButton"; import DouyinLoginButton from "./DouyinLoginButton";
import KwaiLoginButton from "./KwaiLoginButton";
import LoginButton from "./LoginButton"; import LoginButton from "./LoginButton";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import {WechatOfficialAccountModal} from "./Util"; import {WechatOfficialAccountModal} from "./Util";
@ -96,6 +97,8 @@ function getSigninButton(provider) {
return <OktaLoginButton text={text} align={"center"} />; return <OktaLoginButton text={text} align={"center"} />;
} else if (provider.type === "Douyin") { } else if (provider.type === "Douyin") {
return <DouyinLoginButton text={text} align={"center"} />; return <DouyinLoginButton text={text} align={"center"} />;
} else if (provider.type === "Kwai") {
return <KwaiLoginButton text={text} align={"center"} />;
} else { } else {
return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />; return <LoginButton key={provider.type} type={provider.type} logoUrl={getProviderLogoURL(provider)} />;
} }