Compare commits

..

12 Commits

Author SHA1 Message Date
Yaodong Yu
8d686411ee feat: support add providers inside the Organization scope (#1250)
* feat: support add providers inside the Organization scope

Signed-off-by: magicwind <2814461814@qq.com>

* Update ProviderListPage.js

* fix: gloabal admin can see all providers

* fix: table fixed column warning

* fix: edit application page can get all providers

Signed-off-by: magicwind <2814461814@qq.com>
Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-11-04 21:31:08 +08:00
Yaodong Yu
ce722897f1 feat: support prefix path for storage files (#1258) 2022-11-04 21:08:39 +08:00
Mr Forest
a8381e875b feat: change all occurrences when a object name is changed (#1252) 2022-11-02 00:17:38 +08:00
Ke Wang
4c81fd7d16 feat: fix generating wrong x.509 private key file header (#1253)
According to the [official x509 documentation](https://pkg.go.dev/crypto/x509#MarshalPKCS1PrivateKey), the private key generated using `x509.MarshalPKCS1PrivateKey` starts with `-----BEGIN RSA PRIVATE KEY-----` instead of `-----BEGIN PRIVATE KEY-----`. Otherwise, it will not be parsed by most tools (like OpenSSL, [jwt.io](https://jwt.io/), etc.) because it does not conform to the specification.
2022-11-01 22:19:38 +08:00
xAmast
25ee4226d3 feat: clear the session of a signin but non-existent user (#1246) 2022-10-29 20:18:02 +08:00
Bingchang Chen
9d5b019243 fix: nil error if init data is empty (#1247) 2022-10-29 20:04:43 +08:00
Mr Forest
6bb7b545b4 feat: restrict DingTalk user log in who is under the DingTalk Org(which ClientId belong) (#1241)
* feat: fix bug in GetAcceptLanguage()

* feat: add appName when logging in with DingTalk

* fix review problems

* format code

* delete useless printf

* modify display name

Co-authored-by: Gucheng Wang <nomeguy@qq.com>
2022-10-28 22:14:05 +08:00
wenxuan70
25d56ee8d5 feat: allow captcha to be enabled when logging in (#1211)
* Fix bug in GetAcceptLanguage()

* feat: allow captcha to be enabled when logging in

* feat: when the login password is wrong, enable captcha

* feat: Restrict captcha from frontend

* fix: modify CaptchaModal component

* fix: modify the words of i18n

* Update data.json

Co-authored-by: Gucheng Wang <nomeguy@qq.com>
Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-10-28 13:38:14 +08:00
Chell
7e5952c804 fix: login / signin frontend router (#1244)
* fix: go to link

* fix: remove gotologin

* fix: redirect to login page

* fix: redirect to login page

* remove comments

* fix: formats

* fix: formats

* Update Setting.js

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-10-28 02:23:57 +08:00
Yaodong Yu
80bf29d79a feat: fix showing wrong error message: "Please sign in first" (#1245) 2022-10-27 23:50:45 +08:00
Yaodong Yu
971e53dfd8 fix: fix duplicated user bug in user list page (#1243)
* fix: user list repititon errer

* Update UserListPage.js

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-10-27 22:51:05 +08:00
Bingchang Chen
654b903d7a feat: fix multi-platform docker image (#1242) 2022-10-26 23:31:00 +08:00
62 changed files with 1248 additions and 340 deletions

View File

@@ -14,6 +14,9 @@ RUN ./build.sh
FROM alpine:latest AS STANDARD
LABEL MAINTAINER="https://casdoor.org/"
ARG USER=casdoor
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add --update sudo
@@ -28,7 +31,7 @@ RUN adduser -D $USER -u 1000 \
USER 1000
WORKDIR /
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server ./server
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/server_${BUILDX_ARCH} ./server
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/swagger ./swagger
COPY --from=BACK --chown=$USER:$USER /go/src/casdoor/conf/app.conf ./conf/app.conf
COPY --from=FRONT --chown=$USER:$USER /web/build ./web/build
@@ -46,12 +49,15 @@ RUN apt update \
FROM db AS ALLINONE
LABEL MAINTAINER="https://casdoor.org/"
ARG TARGETOS
ARG TARGETARCH
ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}"
RUN apt update
RUN apt install -y ca-certificates && update-ca-certificates
WORKDIR /
COPY --from=BACK /go/src/casdoor/server ./server
COPY --from=BACK /go/src/casdoor/server_${BUILDX_ARCH} ./server
COPY --from=BACK /go/src/casdoor/swagger ./swagger
COPY --from=BACK /go/src/casdoor/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=BACK /go/src/casdoor/conf/app.conf ./conf/app.conf

View File

@@ -8,4 +8,5 @@ else
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
export GOPROXY="https://goproxy.cn,direct"
fi
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server_linux_amd64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o server_linux_arm64 .

View File

@@ -14,6 +14,8 @@
package captcha
import "fmt"
type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error)
}
@@ -32,3 +34,12 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
}
return nil
}
func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) {
provider := GetCaptchaProvider(captchaType)
if provider == nil {
return false, fmt.Errorf("invalid captcha provider: %s", captchaType)
}
return provider.VerifyCaptcha(token, clientSecret)
}

View File

@@ -64,6 +64,10 @@ type RequestForm struct {
RelayState string `json:"relayState"`
SamlRequest string `json:"samlRequest"`
SamlResponse string `json:"samlResponse"`
CaptchaType string `json:"captchaType"`
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
}
type Response struct {
@@ -241,8 +245,7 @@ func (c *ApiController) Logout() {
util.LogInfo(c.Ctx, "API: [%s] logged out", user)
application := c.GetSessionApplication()
c.SetSessionUsername("")
c.SetSessionData(nil)
c.ClearUserSession()
if application == nil || application.Name == "app-built-in" || application.HomepageUrl == "" {
c.ResponseOk(user)

View File

@@ -23,6 +23,8 @@ import (
"strings"
"time"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object"
@@ -251,6 +253,25 @@ func (c *ApiController) Login() {
return
}
} else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
if application == nil {
c.ResponseError(fmt.Sprintf("The application: %s does not exist", form.Application))
return
}
if object.CheckToEnableCaptcha(application) {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
}
if !isHuman {
c.ResponseError("Turing test failed.")
return
}
}
password := form.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage())
}

View File

@@ -63,8 +63,7 @@ func (c *ApiController) GetSessionUsername() string {
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
c.ClearUserSession()
return ""
}
@@ -85,13 +84,17 @@ func (c *ApiController) GetSessionApplication() *object.Application {
return application
}
func (c *ApiController) ClearUserSession() {
c.SetSessionUsername("")
c.SetSessionData(nil)
}
func (c *ApiController) GetSessionOidc() (string, string) {
sessionData := c.GetSessionData()
if sessionData != nil &&
sessionData.ExpireTime != 0 &&
sessionData.ExpireTime < time.Now().Unix() {
c.SetSessionUsername("")
c.SetSessionData(nil)
c.ClearUserSession()
return "", ""
}
scopeValue := c.GetSession("scope")

View File

@@ -48,6 +48,30 @@ func (c *ApiController) GetProviders() {
}
}
// GetGlobalProviders
// @Title GetGlobalProviders
// @Tag Provider API
// @Description get Global providers
// @Success 200 {array} object.Provider The Response object
// @router /get-global-providers [get]
func (c *ApiController) GetGlobalProviders() {
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetMaskedProviders(object.GetGlobalProviders())
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetGlobalProviderCount(field, value)))
providers := object.GetMaskedProviders(object.GetPaginationGlobalProviders(paginator.Offset(), limit, field, value, sortField, sortOrder))
c.ResponseOk(providers, paginator.Nums())
}
}
// GetProvider
// @Title GetProvider
// @Tag Provider API

View File

@@ -83,7 +83,7 @@ func (c *ApiController) SetTokenErrorHttpStatus() {
func (c *ApiController) RequireSignedIn() (string, bool) {
userId := c.GetSessionUsername()
if userId == "" {
c.ResponseError(c.T("LoginErr.SignInFirst"))
c.ResponseError(c.T("LoginErr.LoginFirst"), "Please login first")
return "", false
}
return userId, true
@@ -98,6 +98,7 @@ func (c *ApiController) RequireSignedInUser() (*object.User, bool) {
user := object.GetUser(userId)
if user == nil {
c.ClearUserSession()
c.ResponseError(fmt.Sprintf(c.T("UserErr.DoNotExist"), userId))
return nil, false
}

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = The account for provider: %s and username: %s (%s) is already linked t
ProviderCanNotSignUp = The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up
SessionOutdated = Session outdated, please login again
SignOutFirst = Please sign out first before signing in
SignInFirst = Please sign in first
UserDoNotExist = The user: %s/%s doesn't exist
UserIsForbidden = The user is forbidden to sign in, please contact the administrator
UnknownAuthentication = Unknown authentication type (not password or provider), form = %s

View File

@@ -51,7 +51,6 @@ OldUser = 提供商账户: %s 与用户名: %s (%s) 已经与其他账户绑定:
ProviderCanNotSignUp = 提供商账户: %s 与用户名: %s (%s) 不存在且 不允许通过 %s 注册新账户, 请使用其他方式注册
SignOutFirst = 请在登录前登出
SessionOutdated = Session已过期请重新登陆
SignInFirst = 请先登出
UserDoNotExist = 用户不存在: %s/%s
UserIsForbidden = 该用户被禁止登陆,请联系管理员
UnknownAuthentication = 未知的认证类型 (非密码或提供商认证), form = %s

View File

@@ -15,9 +15,12 @@
package idp
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
@@ -167,7 +170,10 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
Email: dtUserInfo.Email,
AvatarUrl: dtUserInfo.AvatarUrl,
}
isUserInOrg, err := idp.isUserInOrg(userInfo.UnionId)
if !isUserInOrg {
return nil, err
}
return &userInfo, nil
}
@@ -194,3 +200,62 @@ func (idp *DingTalkIdProvider) postWithBody(body interface{}, url string) ([]byt
return data, nil
}
func (idp *DingTalkIdProvider) getInnerAppAccessToken() string {
appKey := idp.Config.ClientID
appSecret := idp.Config.ClientSecret
body := make(map[string]string)
body["appKey"] = appKey
body["appSecret"] = appSecret
bodyData, err := json.Marshal(body)
if err != nil {
log.Println(err.Error())
}
reader := bytes.NewReader(bodyData)
request, err := http.NewRequest("POST", "https://api.dingtalk.com/v1.0/oauth2/accessToken", reader)
request.Header.Set("Content-Type", "application/json;charset=UTF-8")
resp, err := idp.Client.Do(request)
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err.Error())
}
var data struct {
ExpireIn int `json:"expireIn"`
AccessToken string `json:"accessToken"`
}
err = json.Unmarshal(respBytes, &data)
if err != nil {
log.Println(err.Error())
}
return data.AccessToken
}
func (idp *DingTalkIdProvider) isUserInOrg(unionId string) (bool, error) {
body := make(map[string]string)
body["unionid"] = unionId
bodyData, err := json.Marshal(body)
if err != nil {
log.Println(err.Error())
}
reader := bytes.NewReader(bodyData)
accessToken := idp.getInnerAppAccessToken()
request, _ := http.NewRequest("POST", "https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token="+accessToken, reader)
request.Header.Set("Content-Type", "application/json;charset=UTF-8")
resp, err := idp.Client.Do(request)
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err.Error())
}
var data struct {
ErrCode int `json:"errcode"`
ErrMessage string `json:"errmsg"`
}
err = json.Unmarshal(respBytes, &data)
if err != nil {
log.Println(err.Error())
}
if data.ErrCode == 60121 {
return false, fmt.Errorf("the user is not found in the organization where clientId and clientSecret belong")
}
return true, nil
}

View File

@@ -270,6 +270,13 @@ func UpdateApplication(id string, application *Application) bool {
application.Name = name
}
if name != application.Name {
err := applicationChangeTrigger(name, application.Name)
if err != nil {
return false
}
}
for _, providerItem := range application.Providers {
providerItem.Provider = nil
}
@@ -400,3 +407,55 @@ func ExtendManagedAccountsWithUser(user *User) *User {
return user
}
func applicationChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
organization := new(Organization)
organization.DefaultApplication = newName
_, err = session.Where("default_application=?", oldName).Update(organization)
if err != nil {
return err
}
user := new(User)
user.SignupApplication = newName
_, err = session.Where("signup_application=?", oldName).Update(user)
if err != nil {
return err
}
resource := new(Resource)
resource.Application = newName
_, err = session.Where("application=?", oldName).Update(resource)
if err != nil {
return err
}
var permissions []*Permission
err = adapter.Engine.Find(&permissions)
if err != nil {
return err
}
for i := 0; i < len(permissions); i++ {
permissionResoureces := permissions[i].Resources
for j := 0; j < len(permissionResoureces); j++ {
if permissionResoureces[j] == oldName {
permissionResoureces[j] = newName
}
}
permissions[i].Resources = permissionResoureces
_, err = session.Where("name=?", permissions[i].Name).Update(permissions[i])
if err != nil {
return err
}
}
return session.Commit()
}

View File

@@ -114,6 +114,12 @@ func UpdateCert(id string, cert *Cert) bool {
return false
}
if name != cert.Name {
err := certChangeTrigger(name, cert.Name)
if err != nil {
return false
}
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(cert)
if err != nil {
panic(err)
@@ -161,3 +167,22 @@ func getCertByApplication(application *Application) *Cert {
func GetDefaultCert() *Cert {
return getCert("admin", "cert-built-in")
}
func certChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
application := new(Application)
application.Cert = newName
_, err = session.Where("cert=?", oldName).Update(application)
if err != nil {
return err
}
return session.Commit()
}

View File

@@ -340,3 +340,20 @@ func CheckUsername(username string, lang string) string {
return ""
}
func CheckToEnableCaptcha(application *Application) bool {
if len(application.Providers) == 0 {
return false
}
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
continue
}
if providerItem.Provider.Category == "Captcha" && providerItem.Provider.Type == "Default" {
return providerItem.Rule == "Always"
}
}
return false
}

View File

@@ -143,7 +143,7 @@ func initBuiltInApplication() {
EnablePassword: true,
EnableSignUp: true,
Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Provider: nil},
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Rule: "None", Provider: nil},
},
SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},

View File

@@ -56,12 +56,40 @@ func readInitDataFromFile(filePath string) *InitData {
s := util.ReadStringFromPath(filePath)
data := &InitData{}
data := &InitData{
Organizations: []*Organization{},
Applications: []*Application{},
Users: []*User{},
Certs: []*Cert{},
Providers: []*Provider{},
Ldaps: []*Ldap{},
}
err := util.JsonToStruct(s, data)
if err != nil {
panic(err)
}
// transform nil slice to empty slice
for _, organization := range data.Organizations {
if organization.Tags == nil {
organization.Tags = []string{}
}
}
for _, application := range data.Applications {
if application.Providers == nil {
application.Providers = []*ProviderItem{}
}
if application.SignupItems == nil {
application.SignupItems = []*SignupItem{}
}
if application.GrantTypes == nil {
application.GrantTypes = []string{}
}
if application.RedirectUris == nil {
application.RedirectUris = []string{}
}
}
return data
}

View File

@@ -92,6 +92,12 @@ func UpdateModel(id string, modelObj *Model) bool {
return false
}
if name != modelObj.Name {
err := modelChangeTrigger(name, modelObj.Name)
if err != nil {
return false
}
}
// check model grammar
_, err := model.NewModelFromString(modelObj.ModelText)
if err != nil {
@@ -127,3 +133,22 @@ func DeleteModel(model *Model) bool {
func (model *Model) GetId() string {
return fmt.Sprintf("%s/%s", model.Owner, model.Name)
}
func modelChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
permission := new(Permission)
permission.Model = newName
_, err = session.Where("model=?", oldName).Update(permission)
if err != nil {
return err
}
return session.Commit()
}

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n"
@@ -134,15 +135,10 @@ func UpdateOrganization(id string, organization *Organization) bool {
}
if name != organization.Name {
go func() {
application := new(Application)
application.Organization = organization.Name
_, _ = adapter.Engine.Where("organization=?", name).Update(application)
user := new(User)
user.Owner = organization.Name
_, _ = adapter.Engine.Where("owner=?", name).Update(user)
}()
err := organizationChangeTrigger(name, organization.Name)
if err != nil {
return false
}
}
if organization.MasterPassword != "" && organization.MasterPassword != "***" {
@@ -252,3 +248,148 @@ func GetDefaultApplication(id string) (*Application, error) {
return defaultApplication, nil
}
func organizationChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
application := new(Application)
application.Organization = newName
_, err = session.Where("organization=?", oldName).Update(application)
if err != nil {
return err
}
user := new(User)
user.Owner = newName
_, err = session.Where("owner=?", oldName).Update(user)
if err != nil {
return err
}
role := new(Role)
_, err = adapter.Engine.Where("owner=?", oldName).Get(role)
if err != nil {
return err
}
for i, u := range role.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
role.Users[i] = split[0] + "/" + split[1]
}
}
for i, u := range role.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
role.Roles[i] = split[0] + "/" + split[1]
}
}
role.Owner = newName
_, err = session.Where("owner=?", oldName).Update(role)
if err != nil {
return err
}
permission := new(Permission)
_, err = adapter.Engine.Where("owner=?", oldName).Get(permission)
if err != nil {
return err
}
for i, u := range permission.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
permission.Users[i] = split[0] + "/" + split[1]
}
}
for i, u := range permission.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[0] == oldName {
split[0] = newName
permission.Roles[i] = split[0] + "/" + split[1]
}
}
permission.Owner = newName
_, err = session.Where("owner=?", oldName).Update(permission)
if err != nil {
return err
}
casbinAdapter := new(CasbinAdapter)
casbinAdapter.Owner = newName
casbinAdapter.Organization = newName
_, err = session.Where("owner=?", oldName).Update(casbinAdapter)
if err != nil {
return err
}
ldap := new(Ldap)
ldap.Owner = newName
_, err = session.Where("owner=?", oldName).Update(ldap)
if err != nil {
return err
}
model := new(Model)
model.Owner = newName
_, err = session.Where("owner=?", oldName).Update(model)
if err != nil {
return err
}
payment := new(Payment)
payment.Organization = newName
_, err = session.Where("organization=?", oldName).Update(payment)
if err != nil {
return err
}
record := new(Record)
record.Owner = newName
record.Organization = newName
_, err = session.Where("organization=?", oldName).Update(record)
if err != nil {
return err
}
resource := new(Resource)
resource.Owner = newName
_, err = session.Where("owner=?", oldName).Update(resource)
if err != nil {
return err
}
syncer := new(Syncer)
syncer.Organization = newName
_, err = session.Where("organization=?", oldName).Update(syncer)
if err != nil {
return err
}
token := new(Token)
token.Organization = newName
_, err = session.Where("organization=?", oldName).Update(token)
if err != nil {
return err
}
webhook := new(Webhook)
webhook.Organization = newName
_, err = session.Where("organization=?", oldName).Update(webhook)
if err != nil {
return err
}
return session.Commit()
}

View File

@@ -60,6 +60,7 @@ type Provider struct {
IntranetEndpoint string `xorm:"varchar(100)" json:"intranetEndpoint"`
Domain string `xorm:"varchar(100)" json:"domain"`
Bucket string `xorm:"varchar(100)" json:"bucket"`
PathPrefix string `xorm:"varchar(100)" json:"pathPrefix"`
Metadata string `xorm:"mediumtext" json:"metadata"`
IdP string `xorm:"mediumtext" json:"idP"`
@@ -101,6 +102,16 @@ func GetProviderCount(owner, field, value string) int {
return int(count)
}
func GetGlobalProviderCount(field, value string) int {
session := GetSession("", -1, -1, field, value, "", "")
count, err := session.Count(&Provider{})
if err != nil {
panic(err)
}
return int(count)
}
func GetProviders(owner string) []*Provider {
providers := []*Provider{}
err := adapter.Engine.Desc("created_time").Find(&providers, &Provider{Owner: owner})
@@ -111,8 +122,18 @@ func GetProviders(owner string) []*Provider {
return providers
}
func GetPaginationProviders(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Provider {
func GetGlobalProviders() []*Provider {
providers := []*Provider{}
err := adapter.Engine.Desc("created_time").Find(&providers)
if err != nil {
panic(err)
}
return providers
}
func GetPaginationProviders(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Provider {
var providers []*Provider
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&providers)
if err != nil {
@@ -122,6 +143,17 @@ func GetPaginationProviders(owner string, offset, limit int, field, value, sortF
return providers
}
func GetPaginationGlobalProviders(offset, limit int, field, value, sortField, sortOrder string) []*Provider {
var providers []*Provider
session := GetSession("", offset, limit, field, value, sortField, sortOrder)
err := session.Find(&providers)
if err != nil {
panic(err)
}
return providers
}
func getProvider(owner string, name string) *Provider {
if owner == "" || name == "" {
return nil
@@ -175,6 +207,13 @@ func UpdateProvider(id string, provider *Provider) bool {
return false
}
if name != provider.Name {
err := providerChangeTrigger(name, provider.Name)
if err != nil {
return false
}
}
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
if provider.ClientSecret == "***" {
session = session.Omit("client_secret")
@@ -262,3 +301,41 @@ func GetCaptchaProviderByApplication(applicationId, isCurrentProvider, lang stri
}
return nil, nil
}
func providerChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
var applications []*Application
err = adapter.Engine.Find(&applications)
if err != nil {
return err
}
for i := 0; i < len(applications); i++ {
providers := applications[i].Providers
for j := 0; j < len(providers); j++ {
if providers[j].Name == oldName {
providers[j].Name = newName
}
}
applications[i].Providers = providers
_, err = session.Where("name=?", applications[i].Name).Update(applications[i])
if err != nil {
return err
}
}
resource := new(Resource)
resource.Provider = newName
_, err = session.Where("provider=?", oldName).Update(resource)
if err != nil {
return err
}
return session.Commit()
}

View File

@@ -21,6 +21,7 @@ type ProviderItem struct {
CanUnlink bool `json:"canUnlink"`
Prompted bool `json:"prompted"`
AlertType string `json:"alertType"`
Rule string `json:"rule"`
Provider *Provider `json:"provider"`
}

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"strings"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
@@ -94,6 +95,13 @@ func UpdateRole(id string, role *Role) bool {
return false
}
if name != role.Name {
err := roleChangeTrigger(name, role.Name)
if err != nil {
return false
}
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(role)
if err != nil {
panic(err)
@@ -133,3 +141,54 @@ func GetRolesByUser(userId string) []*Role {
return roles
}
func roleChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
var roles []*Role
err = adapter.Engine.Find(&roles)
if err != nil {
return err
}
for _, role := range roles {
for j, u := range role.Roles {
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
role.Roles[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", role.Name).Update(role)
if err != nil {
return err
}
}
var permissions []*Permission
err = adapter.Engine.Find(&permissions)
if err != nil {
return err
}
for _, permission := range permissions {
for j, u := range permission.Roles {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
permission.Roles[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", permission.Name).Update(permission)
if err != nil {
return err
}
}
return session.Commit()
}

View File

@@ -55,7 +55,7 @@ func escapePath(path string) string {
}
func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
escapedPath := escapePath(fullFilePath)
escapedPath := util.UrlJoin(provider.PathPrefix, escapePath(fullFilePath))
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
host := ""
@@ -70,7 +70,7 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
host = util.UrlJoin(provider.Domain, "/files")
}
if provider.Type == "Azure Blob" {
host = fmt.Sprintf("%s/%s", host, provider.Bucket)
host = util.UrlJoin(host, provider.Bucket)
}
fileUrl := util.UrlJoin(host, escapePath(objectKey))

View File

@@ -37,7 +37,7 @@ func generateRsaKeys(bitSize int, expireInYears int, commonName string, organiza
// Encode private key to PKCS#1 ASN.1 PEM.
privateKeyPem := pem.EncodeToMemory(
&pem.Block{
Type: "PRIVATE KEY",
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
},
)

View File

@@ -380,6 +380,13 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
return false
}
if name != user.Name {
err := userChangeTrigger(name, user.Name)
if err != nil {
return false
}
}
if user.Password == "***" {
user.Password = oldUser.Password
}
@@ -416,6 +423,13 @@ func UpdateUserForAllFields(id string, user *User) bool {
return false
}
if name != user.Name {
err := userChangeTrigger(name, user.Name)
if err != nil {
return false
}
}
user.UpdateUserHash()
if user.Avatar != oldUser.Avatar && user.Avatar != "" {
@@ -567,3 +581,62 @@ func ExtendUserWithRolesAndPermissions(user *User) {
user.Roles = GetRolesByUser(user.GetId())
user.Permissions = GetPermissionsByUser(user.GetId())
}
func userChangeTrigger(oldName string, newName string) error {
session := adapter.Engine.NewSession()
defer session.Close()
err := session.Begin()
if err != nil {
return err
}
var roles []*Role
err = adapter.Engine.Find(&roles)
if err != nil {
return err
}
for _, role := range roles {
for j, u := range role.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
role.Users[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", role.Name).Update(role)
if err != nil {
return err
}
}
var permissions []*Permission
err = adapter.Engine.Find(&permissions)
if err != nil {
return err
}
for _, permission := range permissions {
for j, u := range permission.Users {
// u = organization/username
split := strings.Split(u, "/")
if split[1] == oldName {
split[1] = newName
permission.Users[j] = split[0] + "/" + split[1]
}
}
_, err = session.Where("name=?", permission.Name).Update(permission)
if err != nil {
return err
}
}
resource := new(Resource)
resource.User = newName
_, err = session.Where("user=?", oldName).Update(resource)
if err != nil {
return err
}
return session.Commit()
}

View File

@@ -123,6 +123,7 @@ func initAPI() {
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider")
beego.Router("/api/get-global-providers", &controllers.ApiController{}, "GET:GetGlobalProviders")
beego.Router("/api/update-provider", &controllers.ApiController{}, "POST:UpdateProvider")
beego.Router("/api/add-provider", &controllers.ApiController{}, "POST:AddProvider")
beego.Router("/api/delete-provider", &controllers.ApiController{}, "POST:DeleteProvider")

View File

@@ -70,21 +70,6 @@ class AdapterListPage extends BaseListPage {
renderTable(adapters) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@@ -101,6 +86,21 @@ class AdapterListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@@ -221,10 +221,9 @@ class App extends Component {
if (res.status === "ok") {
account = res.data;
account.organization = res.data2;
this.setLanguage(account);
} else {
if (res.msg !== "Please sign in first") {
if (res.data !== "Please login first") {
Setting.showMessage("error", `Failed to sign in: ${res.msg}`);
}
}
@@ -421,13 +420,6 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
{i18next.t("general:Providers")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/applications">
<Link to="/applications">
@@ -438,6 +430,13 @@ class App extends Component {
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
{i18next.t("general:Providers")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/resources">
<Link to="/resources">
@@ -567,6 +566,7 @@ class App extends Component {
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />

View File

@@ -91,6 +91,7 @@ class ApplicationEditPage extends React.Component {
super(props);
this.state = {
classes: props,
owner: props.account.owner,
applicationName: props.match.params.applicationName,
application: null,
organizations: [],
@@ -141,12 +142,21 @@ class ApplicationEditPage extends React.Component {
}
getProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res,
if (Setting.isAdminUser(this.props.account)) {
ProviderBackend.getGlobalProviders()
.then((res) => {
this.setState({
providers: res,
});
});
});
} else {
ProviderBackend.getProviders(this.state.owner)
.then((res) => {
this.setState({
providers: res,
});
});
}
}
getSamlMetadata() {

View File

@@ -63,21 +63,6 @@ class ModelListPage extends BaseListPage {
renderTable(models) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@@ -94,6 +79,21 @@ class ModelListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@@ -76,6 +76,38 @@ class PaymentListPage extends BaseListPage {
renderTable(payments) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "180px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/payments/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
return (
<Link to={`/providers/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
@@ -106,22 +138,7 @@ class PaymentListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "180px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/payments/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
@@ -140,22 +157,6 @@ class PaymentListPage extends BaseListPage {
// sorter: true,
// ...this.getColumnSearchProps('displayName'),
// },
{
title: i18next.t("general:Provider"),
dataIndex: "provider",
key: "provider",
width: "150px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("provider"),
render: (text, record, index) => {
return (
<Link to={`/providers/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("payment:Type"),
dataIndex: "type",

View File

@@ -77,21 +77,7 @@ class PermissionListPage extends BaseListPage {
renderTable(permissions) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
// https://github.com/ant-design/ant-design/issues/22184
{
title: i18next.t("general:Name"),
dataIndex: "name",
@@ -108,6 +94,21 @@ class PermissionListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@@ -32,6 +32,7 @@ class ProviderEditPage extends React.Component {
this.state = {
classes: props,
providerName: props.match.params.providerName,
owner: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
@@ -42,7 +43,7 @@ class ProviderEditPage extends React.Component {
}
getProvider() {
ProviderBackend.getProvider("admin", this.state.providerName)
ProviderBackend.getProvider(this.state.owner, this.state.providerName)
.then((provider) => {
this.setState({
provider: provider,
@@ -469,6 +470,16 @@ class ProviderEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:The prefix path of the file - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.pathPrefix} onChange={e => {
this.updateProviderField("pathPrefix", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :

View File

@@ -23,10 +23,25 @@ import i18next from "i18next";
import BaseListPage from "./BaseListPage";
class ProviderListPage extends BaseListPage {
constructor(props) {
super(props);
this.state = {
classes: props,
owner: Setting.isAdminUser(props.account) ? "admin" : props.account.organization.name,
data: [],
pagination: {
current: 1,
pageSize: 10,
},
loading: false,
searchText: "",
searchedColumn: "",
};
}
newProvider() {
const randomName = Setting.getRandomName();
return {
owner: "admin", // this.props.account.providername,
owner: this.state.owner,
name: `provider_${randomName}`,
createdTime: moment().format(),
displayName: `New Provider - ${randomName}`,
@@ -46,7 +61,7 @@ class ProviderListPage extends BaseListPage {
const newProvider = this.newProvider();
ProviderBackend.addProvider(newProvider)
.then((res) => {
this.props.history.push({pathname: `/providers/${newProvider.name}`, mode: "add"});
this.props.history.push({pathname: `/providers/${newProvider.owner}/${newProvider.name}`, mode: "add"});
}
)
.catch(error => {
@@ -177,7 +192,7 @@ class ProviderListPage extends BaseListPage {
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/providers/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/providers/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete provider: ${record.name} ?`}
onConfirm={() => this.deleteProvider(index)}
@@ -224,7 +239,8 @@ class ProviderListPage extends BaseListPage {
value = params.type;
}
this.setState({loading: true});
ProviderBackend.getProviders("admin", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
(Setting.isAdminUser(this.props.account) ? ProviderBackend.getGlobalProviders(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
: ProviderBackend.getProviders(this.state.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
.then((res) => {
if (res.status === "ok") {
this.setState({

View File

@@ -39,7 +39,7 @@ class ProviderTable extends React.Component {
}
addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None"};
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None", rule: "None"};
if (table === undefined) {
table = [];
}
@@ -193,6 +193,28 @@ class ProviderTable extends React.Component {
// )
// }
// },
{
title: i18next.t("application:Rule"),
dataIndex: "rule",
key: "rule",
width: "100px",
render: (text, record, index) => {
if (record.provider?.category !== "Captcha") {
return null;
}
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
defaultValue="None"
onChange={value => {
this.updateField(table, index, "rule", value);
}} >
<Option key="None" value="None">{i18next.t("application:None")}</Option>
<Option key="Always" value="Always">{i18next.t("application:Always")}</Option>
</Select>
);
},
},
{
title: i18next.t("general:Action"),
key: "action",

View File

@@ -65,21 +65,6 @@ class RoleListPage extends BaseListPage {
renderTable(roles) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@@ -96,6 +81,21 @@ class RoleListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link, useHistory} from "react-router-dom";
import {Tag, Tooltip, message} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons";
import React from "react";
import {isMobile as isMobileDevice} from "react-device-detect";
import "./i18n";
import i18next from "i18next";
@@ -22,7 +23,6 @@ import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import {Link} from "react-router-dom";
import * as path from "path-browserify";
export const ServerUrl = "";
@@ -743,26 +743,31 @@ export function renderLogo(application) {
}
}
export function goToLogin(ths, application) {
export function getLoginLink(application) {
let url;
if (application === null) {
return;
}
if (!application.enablePassword && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
const link = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
goToLink(link);
return;
}
if (authConfig.appName === application.name) {
goToLinkSoft(ths, "/login");
url = null;
} else if (!application.enablePassword && window.location.pathname.includes("/auto-signup/oauth/authorize")) {
url = window.location.href.replace("/auto-signup/oauth/authorize", "/login/oauth/authorize");
} else if (authConfig.appName === application.name) {
url = "/login";
} else if (application.signinUrl === "") {
url = path.join(application.homepageUrl, "login");
} else {
if (application.signinUrl === "") {
goToLink(path.join(application.homepageUrl, "login"));
} else {
goToLink(application.signinUrl);
}
url = application.signinUrl;
}
return url;
}
export function renderLoginLink(application, text) {
const url = getLoginLink(application);
return renderLink(url, text, null);
}
export function redirectToLoginPage(application) {
const loginLink = getLoginLink(application);
const history = useHistory();
history.push(loginLink);
}
function renderLink(url, text, onClick) {

View File

@@ -164,7 +164,7 @@ class SignupTable extends React.Component {
},
},
{
title: i18next.t("application:rule"),
title: i18next.t("application:Rule"),
dataIndex: "rule",
key: "rule",
width: "155px",

View File

@@ -88,21 +88,6 @@ class SyncerListPage extends BaseListPage {
renderTable(syncers) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@@ -119,6 +104,21 @@ class SyncerListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "120px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@@ -352,7 +352,7 @@ class UserListPage extends BaseListPage {
return (
<div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={users} rowKey="name" size="middle" bordered pagination={paginationProps}
<Table scroll={{x: "max-content"}} columns={columns} dataSource={users} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Users")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@@ -67,21 +67,6 @@ class WebhookListPage extends BaseListPage {
renderTable(webhooks) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "110px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Name"),
dataIndex: "name",
@@ -98,6 +83,21 @@ class WebhookListPage extends BaseListPage {
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "organization",
key: "organization",
width: "110px",
sorter: true,
...this.getColumnSearchProps("organization"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",

View File

@@ -166,7 +166,7 @@ class ForgetPage extends React.Component {
values.userOwner = this.state.application?.organizationObj.name;
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword).then(res => {
if (res.status === "ok") {
Setting.goToLogin(this, this.state.application);
Setting.redirectToLoginPage(this.state.application);
} else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
}

View File

@@ -13,7 +13,6 @@
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
import {LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
@@ -30,6 +29,7 @@ import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput";
import SelectLanguageBox from "../SelectLanguageBox";
import {withTranslation} from "react-i18next";
import {CaptchaModal} from "../common/CaptchaModal";
const {TabPane} = Tabs;
@@ -49,6 +49,9 @@ class LoginPage extends React.Component {
validEmail: false,
validPhone: false,
loginMethod: "password",
enableCaptchaModal: false,
openCaptchaModal: false,
verifyCaptcha: undefined,
};
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@@ -69,6 +72,18 @@ class LoginPage extends React.Component {
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.application && !prevState.application) {
const defaultCaptchaProviderItems = this.getDefaultCaptchaProviderItems(this.state.application);
if (!defaultCaptchaProviderItems) {
return;
}
this.setState({enableCaptchaModal: defaultCaptchaProviderItems.some(providerItem => providerItem.rule === "Always")});
}
}
getApplicationLogin() {
const oAuthParams = Util.getOAuthGetParameters();
AuthBackend.getApplicationLogin(oAuthParams)
@@ -226,6 +241,23 @@ class LoginPage extends React.Component {
return;
}
if (this.state.loginMethod === "password" && this.state.enableCaptchaModal) {
this.setState({
openCaptchaModal: true,
verifyCaptcha: (captchaType, captchaToken, secret) => {
values["captchaType"] = captchaType;
values["captchaToken"] = captchaToken;
values["clientSecret"] = secret;
this.login(values);
},
});
} else {
this.login(values);
}
}
login(values) {
// here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server
if (this.state.type === "cas") {
// CAS
@@ -240,6 +272,8 @@ class LoginPage extends React.Component {
}
Util.showMessage("success", msg);
this.setState({openCaptchaModal: false});
if (casParams.service !== "") {
const st = res.data;
const newUrl = new URL(casParams.service);
@@ -247,6 +281,7 @@ class LoginPage extends React.Component {
window.location.href = newUrl.toString();
}
} else {
this.setState({openCaptchaModal: false});
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
@@ -259,6 +294,7 @@ class LoginPage extends React.Component {
.then((res) => {
if (res.status === "ok") {
const responseType = values["type"];
if (responseType === "login") {
Util.showMessage("success", "Logged in successfully");
@@ -276,6 +312,7 @@ class LoginPage extends React.Component {
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
}
} else {
this.setState({openCaptchaModal: false});
Util.showMessage("error", `Failed to log in: ${res.msg}`);
}
});
@@ -302,13 +339,11 @@ class LoginPage extends React.Component {
title="Sign Up Error"
subTitle={"The application does not allow to sign up new account"}
extra={[
<Link key="login" onClick={() => {
Setting.goToLogin(this, application);
}}>
<Button type="primary" key="signin">
Sign In
</Button>
</Link>,
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application)}>
{
i18next.t("login:Sign In")
}
</Button>,
]}
>
</Result>
@@ -421,6 +456,9 @@ class LoginPage extends React.Component {
i18next.t("login:Sign In")
}
</Button>
{
this.renderCaptchaModal(application)
}
{
this.renderFooter(application)
}
@@ -463,16 +501,54 @@ class LoginPage extends React.Component {
}
}
getDefaultCaptchaProviderItems(application) {
const providers = application?.providers;
if (providers === undefined || providers === null) {
return null;
}
return providers.filter(providerItem => {
if (providerItem.provider === undefined || providerItem.provider === null) {
return false;
}
return providerItem.provider.category === "Captcha" && providerItem.provider.type === "Default";
});
}
renderCaptchaModal(application) {
if (!this.state.enableCaptchaModal) {
return null;
}
const provider = this.getDefaultCaptchaProviderItems(application)
.filter(providerItem => providerItem.rule === "Always")
.map(providerItem => providerItem.provider)[0];
return <CaptchaModal
owner={provider.owner}
name={provider.name}
captchaType={provider.type}
subType={provider.subType}
clientId={provider.clientId}
clientId2={provider.clientId2}
clientSecret={provider.clientSecret}
clientSecret2={provider.clientSecret2}
open={this.state.openCaptchaModal}
onOk={(captchaType, captchaToken, secret) => this.state.verifyCaptcha?.(captchaType, captchaToken, secret)}
canCancel={false}
/>;
}
renderFooter(application) {
if (this.state.mode === "signup") {
return (
<div style={{float: "right"}}>
{i18next.t("signup:Have account?")}&nbsp;
<Link onClick={() => {
Setting.goToLogin(this, application);
}}>
{i18next.t("signup:sign in now")}
</Link>
{
Setting.renderLoginLink(application, i18next.t("signup:sign in now"))
}
</div>
);
} else {

View File

@@ -190,7 +190,7 @@ class PromptPage extends React.Component {
if (redirectUrl !== "" && redirectUrl !== null) {
Setting.goToLink(redirectUrl);
} else {
Setting.goToLogin(this, this.getApplicationObj());
Setting.redirectToLoginPage(this.getApplicationObj());
}
} else {
Setting.showMessage("error", `Failed to log out: ${res.msg}`);
@@ -234,10 +234,10 @@ class PromptPage extends React.Component {
title="Sign Up Error"
subTitle={"You are unexpected to see this prompt page"}
extra={[
<Button type="primary" key="signin" onClick={() => {
Setting.goToLogin(this, application);
}}>
Sign In
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application)}>
{
i18next.t("login:Sign In")
}
</Button>,
]}
>

View File

@@ -69,7 +69,7 @@ class ResultPage extends React.Component {
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage);
} else {
Setting.goToLogin(this, application);
Setting.redirectToLoginPage(application);
}
}}>
{i18next.t("login:Sign In")}

View File

@@ -541,10 +541,10 @@ class SignupPage extends React.Component {
title="Sign Up Error"
subTitle={"The application does not allow to sign up new account"}
extra={[
<Button type="primary" key="signin" onClick={() => {
Setting.goToLogin(this, application);
}}>
Sign In
<Button type="primary" key="signin" onClick={() => Setting.redirectToLoginPage(application)}>
{
i18next.t("login:Sign In")
}
</Button>,
]}
>
@@ -600,7 +600,7 @@ class SignupPage extends React.Component {
if (linkInStorage !== null && linkInStorage !== "") {
Setting.goToLink(linkInStorage);
} else {
Setting.goToLogin(this, application);
Setting.redirectToLoginPage(application);
}
}}>
{i18next.t("signup:sign in now")}

View File

@@ -24,6 +24,16 @@ export function getProviders(owner, page = "", pageSize = "", field = "", value
}).then(res => res.json());
}
export function getGlobalProviders(page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-global-providers?p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function getProvider(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-provider?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",

View File

@@ -0,0 +1,159 @@
// Copyright 2022 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 {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import * as UserBackend from "../backend/UserBackend";
import {CaptchaWidget} from "./CaptchaWidget";
import {SafetyOutlined} from "@ant-design/icons";
export const CaptchaModal = ({
owner,
name,
captchaType,
subType,
clientId,
clientId2,
clientSecret,
clientSecret2,
open,
onOk,
onCancel,
canCancel,
}) => {
const [visible, setVisible] = React.useState(false);
const [captchaImg, setCaptchaImg] = React.useState("");
const [captchaToken, setCaptchaToken] = React.useState("");
const [secret, setSecret] = React.useState(clientSecret);
const [secret2, setSecret2] = React.useState(clientSecret2);
useEffect(() => {
setVisible(() => {
if (open) {
getCaptchaFromBackend();
} else {
cleanUp();
}
return open;
});
}, [open]);
const handleOk = () => {
onOk?.(captchaType, captchaToken, secret);
};
const handleCancel = () => {
onCancel?.();
};
const cleanUp = () => {
setCaptchaToken("");
};
const getCaptchaFromBackend = () => {
UserBackend.getCaptcha(owner, name, true).then((res) => {
if (captchaType === "Default") {
setSecret(res.captchaId);
setCaptchaImg(res.captchaImage);
} else {
setSecret(res.clientSecret);
setSecret2(res.clientSecret2);
}
});
};
const renderDefaultCaptcha = () => {
return (
<Col>
<Row
style={{
backgroundImage: `url('data:image/png;base64,${captchaImg}')`,
backgroundRepeat: "no-repeat",
height: "80px",
width: "200px",
borderRadius: "5px",
border: "1px solid #ccc",
marginBottom: 10,
}}
/>
<Row>
<Input
autoFocus
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onPressEnter={handleOk}
onChange={(e) => setCaptchaToken(e.target.value)}
/>
</Row>
</Col>
);
};
const onSubmit = (token) => {
setCaptchaToken(token);
};
const renderCheck = () => {
if (captchaType === "Default") {
return renderDefaultCaptcha();
} else {
return (
<Col>
<Row>
<CaptchaWidget
captchaType={captchaType}
subType={subType}
siteKey={clientId}
clientSecret={secret}
onChange={onSubmit}
clientId2={clientId2}
clientSecret2={secret2}
/>
</Row>
</Col>
);
}
};
const renderFooter = () => {
if (canCancel) {
return [
<Button key="cancel" onClick={handleCancel}>{i18next.t("user:Cancel")}</Button>,
<Button key="ok" type="primary" onClick={handleOk}>{i18next.t("user:OK")}</Button>,
];
} else {
return [
<Button key="ok" type="primary" onClick={handleOk}>{i18next.t("user:OK")}</Button>,
];
}
};
return (
<React.Fragment>
<Modal
closable={false}
maskClosable={false}
destroyOnClose={true}
title={i18next.t("general:Captcha")}
visible={visible}
width={348}
footer={renderFooter()}
>
{renderCheck()}
</Modal>
</React.Fragment>
);
};

View File

@@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd";
import {Button} from "antd";
import React from "react";
import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend";
import {CaptchaModal} from "./CaptchaModal";
import * as ProviderBackend from "../backend/ProviderBackend";
import {SafetyOutlined} from "@ant-design/icons";
import {CaptchaWidget} from "./CaptchaWidget";
import * as UserBackend from "../backend/UserBackend";
export const CaptchaPreview = ({
provider,
@@ -33,37 +32,9 @@ export const CaptchaPreview = ({
clientId2,
clientSecret2,
}) => {
const [visible, setVisible] = React.useState(false);
const [captchaImg, setCaptchaImg] = React.useState("");
const [captchaToken, setCaptchaToken] = React.useState("");
const [secret, setSecret] = React.useState(clientSecret);
const [secret2, setSecret2] = React.useState(clientSecret2);
const handleOk = () => {
UserBackend.verifyCaptcha(captchaType, captchaToken, secret).then(() => {
setCaptchaToken("");
setVisible(false);
});
};
const handleCancel = () => {
setVisible(false);
};
const getCaptchaFromBackend = () => {
UserBackend.getCaptcha(owner, name, true).then((res) => {
if (captchaType === "Default") {
setSecret(res.captchaId);
setCaptchaImg(res.captchaImage);
} else {
setSecret(res.clientSecret);
setSecret2(res.clientSecret2);
}
});
};
const [open, setOpen] = React.useState(false);
const clickPreview = () => {
setVisible(true);
provider.name = name;
provider.clientId = clientId;
provider.type = captchaType;
@@ -71,64 +42,10 @@ export const CaptchaPreview = ({
if (clientSecret !== "***") {
provider.clientSecret = clientSecret;
ProviderBackend.updateProvider(owner, providerName, provider).then(() => {
getCaptchaFromBackend();
setOpen(true);
});
} else {
getCaptchaFromBackend();
}
};
const renderDefaultCaptcha = () => {
return (
<Col>
<Row
style={{
backgroundImage: `url('data:image/png;base64,${captchaImg}')`,
backgroundRepeat: "no-repeat",
height: "80px",
width: "200px",
borderRadius: "5px",
border: "1px solid #ccc",
marginBottom: 10,
}}
/>
<Row>
<Input
autoFocus
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onPressEnter={handleOk}
onChange={(e) => setCaptchaToken(e.target.value)}
/>
</Row>
</Col>
);
};
const onSubmit = (token) => {
setCaptchaToken(token);
};
const renderCheck = () => {
if (captchaType === "Default") {
return renderDefaultCaptcha();
} else {
return (
<Col>
<Row>
<CaptchaWidget
captchaType={captchaType}
subType={subType}
siteKey={clientId}
clientSecret={secret}
onChange={onSubmit}
clientId2={clientId2}
clientSecret2={secret2}
/>
</Row>
</Col>
);
setOpen(true);
}
};
@@ -146,6 +63,16 @@ export const CaptchaPreview = ({
return false;
};
const onOk = (captchaType, captchaToken, secret) => {
UserBackend.verifyCaptcha(captchaType, captchaToken, secret).then(() => {
setOpen(false);
});
};
const onCancel = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button
@@ -156,20 +83,20 @@ export const CaptchaPreview = ({
>
{i18next.t("general:Preview")}
</Button>
<Modal
closable={false}
maskClosable={false}
destroyOnClose={true}
title={i18next.t("general:Captcha")}
visible={visible}
okText={i18next.t("user:OK")}
cancelText={i18next.t("user:Cancel")}
onOk={handleOk}
onCancel={handleCancel}
width={348}
>
{renderCheck()}
</Modal>
<CaptchaModal
owner={owner}
name={name}
captchaType={captchaType}
subType={subType}
clientId={clientId}
clientId2={clientId2}
clientSecret={clientSecret}
clientSecret2={clientSecret2}
open={open}
onOk={onOk}
onCancel={onCancel}
canCancel={true}
/>
</React.Fragment>
);
};

View File

@@ -13,6 +13,7 @@
"Sync": "Sync"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "Auto signin - Tooltip",
"Background URL": "Background URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "Grant types - Tooltip",
"Left": "Left",
"New Application": "New Application",
"None": "None",
"Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei",
@@ -53,6 +55,7 @@
"Refresh token expire": "Aktualisierungs-Token läuft ab",
"Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip",
"Right": "Right",
"Rule": "Rule",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@@ -67,8 +70,7 @@
"Token expire": "Token läuft ab",
"Token expire - Tooltip": "Token läuft ab - Tooltip",
"Token format": "Token-Format",
"Token format - Tooltip": "Token-Format - Tooltip",
"rule": "rule"
"Token format - Tooltip": "Token-Format - Tooltip"
},
"cert": {
"Bit size": "Bitgröße",
@@ -489,6 +491,7 @@
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Metadaten erfolgreich analysieren",
"Path prefix": "Path prefix",
"Port": "Port",
"Port - Tooltip": "Unique string-style identifier",
"Provider URL": "Provider-URL",
@@ -533,6 +536,7 @@
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"The prefix path of the file - Tooltip": "The prefix path of the file - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Typ",
@@ -695,6 +699,7 @@
"Old Password": "Altes Passwort",
"Password": "Passwort",
"Password Set": "Passwort setzen",
"Please select avatar from resources": "Please select avatar from resources",
"Properties": "Eigenschaften",
"Re-enter New": "Neu erneut eingeben",
"Reset Email...": "Reset Email...",

View File

@@ -13,6 +13,7 @@
"Sync": "Sync"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "Auto signin - Tooltip",
"Background URL": "Background URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "Grant types - Tooltip",
"Left": "Left",
"New Application": "New Application",
"None": "None",
"Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip",
"Please select a HTML file": "Please select a HTML file",
@@ -53,6 +55,7 @@
"Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"Right": "Right",
"Rule": "Rule",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@@ -67,8 +70,7 @@
"Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip",
"rule": "rule"
"Token format - Tooltip": "Token format - Tooltip"
},
"cert": {
"Bit size": "Bit size",
@@ -489,6 +491,7 @@
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully",
"Path prefix": "Path prefix",
"Port": "Port",
"Port - Tooltip": "Port - Tooltip",
"Provider URL": "Provider URL",
@@ -533,6 +536,7 @@
"Test Connection": "Test Connection",
"Test Email": "Test Email",
"Test Email - Tooltip": "Test Email - Tooltip",
"The prefix path of the file - Tooltip": "The prefix path of the file - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type",
@@ -695,6 +699,7 @@
"Old Password": "Old Password",
"Password": "Password",
"Password Set": "Password Set",
"Please select avatar from resources": "Please select avatar from resources",
"Properties": "Properties",
"Re-enter New": "Re-enter New",
"Reset Email...": "Reset Email...",

View File

@@ -45,7 +45,9 @@
"Token expire - Tooltip": "Expiración del Token - Tooltip",
"Token format": "Formato del Token",
"Token format - Tooltip": "Formato del Token - Tooltip",
"rule": "rule"
"Rule": "Rule",
"None": "None",
"Always": "Always"
},
"cert": {
"Bit size": "Tamaño del Bit",

View File

@@ -13,6 +13,7 @@
"Sync": "Sync"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "Auto signin - Tooltip",
"Background URL": "Background URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "Grant types - Tooltip",
"Left": "Left",
"New Application": "New Application",
"None": "None",
"Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
@@ -53,6 +55,7 @@
"Refresh token expire": "Expiration du jeton d'actualisation",
"Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle",
"Right": "Right",
"Rule": "Rule",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@@ -67,8 +70,7 @@
"Token expire": "Expiration du jeton",
"Token expire - Tooltip": "Expiration du jeton - Info-bulle",
"Token format": "Format du jeton",
"Token format - Tooltip": "Format du jeton - infobulle",
"rule": "rule"
"Token format - Tooltip": "Format du jeton - infobulle"
},
"cert": {
"Bit size": "Taille du bit",
@@ -489,6 +491,7 @@
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Analyse des métadonnées réussie",
"Path prefix": "Path prefix",
"Port": "Port",
"Port - Tooltip": "Unique string-style identifier",
"Provider URL": "URL du fournisseur",
@@ -533,6 +536,7 @@
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"The prefix path of the file - Tooltip": "The prefix path of the file - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type de texte",
@@ -695,6 +699,7 @@
"Old Password": "Ancien mot de passe",
"Password": "Mot de passe",
"Password Set": "Mot de passe défini",
"Please select avatar from resources": "Please select avatar from resources",
"Properties": "Propriétés",
"Re-enter New": "Nouvelle entrée",
"Reset Email...": "Reset Email...",

View File

@@ -13,6 +13,7 @@
"Sync": "Sync"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "Auto signin - Tooltip",
"Background URL": "Background URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "Grant types - Tooltip",
"Left": "Left",
"New Application": "New Application",
"None": "None",
"Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "HTMLファイルを選択してください",
@@ -53,6 +55,7 @@
"Refresh token expire": "トークンの更新の期限が切れます",
"Refresh token expire - Tooltip": "トークンの有効期限を更新する - ツールチップ",
"Right": "Right",
"Rule": "Rule",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@@ -67,8 +70,7 @@
"Token expire": "トークンの有効期限",
"Token expire - Tooltip": "トークンの有効期限 - ツールチップ",
"Token format": "トークンのフォーマット",
"Token format - Tooltip": "トークンフォーマット - ツールチップ",
"rule": "rule"
"Token format - Tooltip": "トークンフォーマット - ツールチップ"
},
"cert": {
"Bit size": "ビットサイズ",
@@ -489,6 +491,7 @@
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "メタデータの解析に成功",
"Path prefix": "Path prefix",
"Port": "ポート",
"Port - Tooltip": "Unique string-style identifier",
"Provider URL": "プロバイダー URL",
@@ -533,6 +536,7 @@
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"The prefix path of the file - Tooltip": "The prefix path of the file - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "タイプ",
@@ -695,6 +699,7 @@
"Old Password": "古いパスワード",
"Password": "パスワード",
"Password Set": "パスワード設定",
"Please select avatar from resources": "Please select avatar from resources",
"Properties": "プロパティー",
"Re-enter New": "新しい入力",
"Reset Email...": "Reset Email...",

View File

@@ -13,6 +13,7 @@
"Sync": "Sync"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "Auto signin - Tooltip",
"Background URL": "Background URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "Grant types - Tooltip",
"Left": "Left",
"New Application": "New Application",
"None": "None",
"Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Please select a HTML file",
@@ -53,6 +55,7 @@
"Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"Right": "Right",
"Rule": "Rule",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@@ -67,8 +70,7 @@
"Token expire": "Token expire",
"Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip",
"rule": "rule"
"Token format - Tooltip": "Token format - Tooltip"
},
"cert": {
"Bit size": "Bit size",
@@ -489,6 +491,7 @@
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully",
"Path prefix": "Path prefix",
"Port": "Port",
"Port - Tooltip": "Unique string-style identifier",
"Provider URL": "Provider URL",
@@ -533,6 +536,7 @@
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"The prefix path of the file - Tooltip": "The prefix path of the file - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Type",
@@ -695,6 +699,7 @@
"Old Password": "Old Password",
"Password": "Password",
"Password Set": "Password Set",
"Please select avatar from resources": "Please select avatar from resources",
"Properties": "Properties",
"Re-enter New": "Re-enter New",
"Reset Email...": "Reset Email...",

View File

@@ -13,6 +13,7 @@
"Sync": "Sync"
},
"application": {
"Always": "Always",
"Auto signin": "Auto signin",
"Auto signin - Tooltip": "Auto signin - Tooltip",
"Background URL": "Background URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "Виды грантов - Подсказка",
"Left": "Left",
"New Application": "Новое приложение",
"None": "None",
"Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Пожалуйста, выберите HTML-файл",
@@ -53,6 +55,7 @@
"Refresh token expire": "Срок действия обновления токена истекает",
"Refresh token expire - Tooltip": "Срок обновления токена истекает - Подсказка",
"Right": "Right",
"Rule": "правило",
"SAML metadata": "Метаданные SAML",
"SAML metadata - Tooltip": "Метаданные SAML - Подсказка",
"SAML metadata URL copied to clipboard successfully": "Адрес метаданных SAML скопирован в буфер обмена",
@@ -67,8 +70,7 @@
"Token expire": "Токен истекает",
"Token expire - Tooltip": "Истек токен - Подсказка",
"Token format": "Формат токена",
"Token format - Tooltip": "Формат токена - Подсказка",
"rule": "правило"
"Token format - Tooltip": "Формат токена - Подсказка"
},
"cert": {
"Bit size": "Размер бита",
@@ -489,6 +491,7 @@
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Анализ метаданных успешно завершен",
"Path prefix": "Path prefix",
"Port": "Порт",
"Port - Tooltip": "Unique string-style identifier",
"Provider URL": "URL провайдера",
@@ -533,6 +536,7 @@
"Test Connection": "Test Smtp Connection",
"Test Email": "Test email config",
"Test Email - Tooltip": "Email Address",
"The prefix path of the file - Tooltip": "The prefix path of the file - Tooltip",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - Tooltip",
"Type": "Тип",
@@ -695,6 +699,7 @@
"Old Password": "Старый пароль",
"Password": "Пароль",
"Password Set": "Пароль установлен",
"Please select avatar from resources": "Please select avatar from resources",
"Properties": "Свойства",
"Re-enter New": "Введите еще раз",
"Reset Email...": "Reset Email...",

View File

@@ -13,6 +13,7 @@
"Sync": "同步"
},
"application": {
"Always": "始终开启",
"Auto signin": "启用自动登录",
"Auto signin - Tooltip": "当Casdoor存在已登录会话时自动采用该会话进行应用端的登录",
"Background URL": "背景图URL",
@@ -43,6 +44,7 @@
"Grant types - Tooltip": "选择允许哪些OAuth协议中的Grant types",
"Left": "居左",
"New Application": "添加应用",
"None": "关闭",
"Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录",
"Please select a HTML file": "请选择一个HTML文件",
@@ -53,6 +55,7 @@
"Refresh token expire": "Refresh Token过期",
"Refresh token expire - Tooltip": "Refresh Token过期时间",
"Right": "居右",
"Rule": "规则",
"SAML metadata": "SAML元数据",
"SAML metadata - Tooltip": "SAML协议的元数据Metadata信息",
"SAML metadata URL copied to clipboard successfully": "SAML元数据URL已成功复制到剪贴板",
@@ -67,8 +70,7 @@
"Token expire": "Access Token过期",
"Token expire - Tooltip": "Access Token过期时间",
"Token format": "Access Token格式",
"Token format - Tooltip": "Access Token格式",
"rule": "规则"
"Token format - Tooltip": "Access Token格式"
},
"cert": {
"Bit size": "位大小",
@@ -489,6 +491,7 @@
"New Provider": "添加提供商",
"Parse": "Parse",
"Parse Metadata successfully": "解析元数据成功",
"Path prefix": "路径前缀",
"Port": "端口",
"Port - Tooltip": "端口号",
"Provider URL": "提供商URL",
@@ -533,6 +536,7 @@
"Test Connection": "测试SMTP连接",
"Test Email": "测试Email配置",
"Test Email - Tooltip": "邮箱地址",
"The prefix path of the file - Tooltip": "文件的路径前缀 - 工具提示",
"Token URL": "Token URL",
"Token URL - Tooltip": "Token URL - 工具提示",
"Type": "类型",
@@ -695,6 +699,7 @@
"Old Password": "旧密码",
"Password": "密码",
"Password Set": "密码已设置",
"Please select avatar from resources": "从资源中选择...",
"Properties": "属性",
"Re-enter New": "重复新密码",
"Reset Email...": "重置邮箱...",