Compare commits

...

22 Commits

Author SHA1 Message Date
a71a190db5 feat: fix bug in redirectToLoginPage() (#1422) 2022-12-24 01:10:02 +08:00
da69d94445 feat: fix the bug that spin in oauth is always showing (#1421) 2022-12-23 15:06:51 +08:00
b8b915abe1 feat: check AccessPermission in multiple permissions (#1420) 2022-12-23 14:06:02 +08:00
5d1548e989 feat: fix absolute URL redirection (#1419)
* fix: redirect to absolute url

* fix: original jump
2022-12-23 11:05:15 +08:00
a0dc6e06cd feat: add EntryPage for login, signup pages to fix background flashing issue (#1416)
* feat: fix flush in login Pages

* fix: code format

* fix: improve code

* Update App.js

* Update EntryPage.js

* fix: optimize api request

* Update App.js

* Update App.js

* fix: fix css

* fix: css and getApllicationObj

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2022-12-22 23:39:02 +08:00
ae130788ec feat: add Line support as OAuth 3rd-party login (#1413) 2022-12-21 02:25:58 +08:00
f075d0fd74 Refactor out application.IsRedirectUriValid() 2022-12-21 00:35:33 +08:00
65d4946042 feat: add valid key for creating token (#1411) 2022-12-20 22:05:00 +08:00
Liu
26acece8af feat: add all other missing objects to init_data (#1407)
* Add all other missing objects to init_data.json

* Format golang code

* feat: add all other missing objects to init_data

* feat: add all other missing objects to init_data
2022-12-18 01:49:42 +08:00
48a0c8473f Improve README 2022-12-18 01:41:12 +08:00
082ae3c91e fix: fix undefined owner bug in AdapterEditPage (#1406) 2022-12-17 21:21:39 +08:00
1ee2ff1d30 feat: now dingtalk OAuth returns all error messages to frontend (#1405) 2022-12-17 21:10:20 +08:00
c0d9969013 Add description to product 2022-12-16 23:35:30 +08:00
1bdee13150 Fix bug in renderQrCodeModal() 2022-12-16 23:28:43 +08:00
d668022af0 feat: fix length of policy and [policy_define] in model inconsistent (#1400) 2022-12-15 20:42:55 +08:00
e227875c2b feat: add post methed for saml response (#1399) 2022-12-13 22:32:45 +08:00
e473de3162 feat: fix sign in error via webauthn (#1398)
* fix: fix sign in error via webauthn

* fix review problems
2022-12-13 16:57:42 +08:00
c5ef841d3f Disable isValidIdCard() 2022-12-12 01:07:31 +08:00
d46288b591 Add renderQrCodeModal() 2022-12-12 00:42:45 +08:00
b968bf033c fix: case insensitive country name and country abbreviation search in region selection (#1394) 2022-12-11 18:14:25 +08:00
eca2527bc0 feat: fix bug in signup and reset phone and email (#1396)
* fix: fix bug in signup and reset phone and email

* delete useless addition
2022-12-11 15:52:36 +08:00
ef836acfe9 fix: login page flag icon preload (#1393) 2022-12-11 11:22:58 +08:00
51 changed files with 927 additions and 255 deletions

View File

@ -44,14 +44,12 @@
## Online demo ## Online demo
- International: https://door.casdoor.org (read-only) - Read-only site: https://door.casdoor.com (any modification operation will fail)
- Asian mirror: https://door.casdoor.com (read-only) - Writable site: https://demo.casdoor.com (original data will be restored for every 5 minutes)
- Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes)
## Documentation ## Documentation
- International: https://casdoor.org https://casdoor.org
- Asian mirror: https://casdoor.cn
## Install ## Install

View File

@ -170,7 +170,7 @@ func (c *ApiController) GetApplicationLogin() {
} }
func setHttpClient(idProvider idp.IdProvider, providerType string) { func setHttpClient(idProvider idp.IdProvider, providerType string) {
if providerType == "GitHub" || providerType == "Google" || providerType == "Facebook" || providerType == "LinkedIn" || providerType == "Steam" { if providerType == "GitHub" || providerType == "Google" || providerType == "Facebook" || providerType == "LinkedIn" || providerType == "Steam" || providerType == "Line" {
idProvider.SetHttpClient(proxy.ProxyHttpClient) idProvider.SetHttpClient(proxy.ProxyHttpClient)
} else { } else {
idProvider.SetHttpClient(proxy.DefaultHttpClient) idProvider.SetHttpClient(proxy.DefaultHttpClient)

View File

@ -261,7 +261,7 @@ func (c *ApiController) TokenLogout() {
flag, application := object.DeleteTokenByAccessToken(token) flag, application := object.DeleteTokenByAccessToken(token)
redirectUri := c.Input().Get("post_logout_redirect_uri") redirectUri := c.Input().Get("post_logout_redirect_uri")
state := c.Input().Get("state") state := c.Input().Get("state")
if application != nil && object.CheckRedirectUriValid(application, redirectUri) { if application != nil && application.IsRedirectUriValid(redirectUri) {
c.Ctx.Redirect(http.StatusFound, redirectUri+"?state="+state) c.Ctx.Redirect(http.StatusFound, redirectUri+"?state="+state)
return return
} }

View File

@ -47,6 +47,7 @@ func (c *ApiController) SendVerificationCode() {
checkKey := c.Ctx.Request.Form.Get("checkKey") checkKey := c.Ctx.Request.Form.Get("checkKey")
checkUser := c.Ctx.Request.Form.Get("checkUser") checkUser := c.Ctx.Request.Form.Get("checkUser")
applicationId := c.Ctx.Request.Form.Get("applicationId") applicationId := c.Ctx.Request.Form.Get("applicationId")
method := c.Ctx.Request.Form.Get("method")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request) remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if destType == "" { if destType == "" {
@ -119,7 +120,7 @@ func (c *ApiController) SendVerificationCode() {
} }
userByEmail := object.GetUserByEmail(organization.Name, dest) userByEmail := object.GetUserByEmail(organization.Name, dest)
if userByEmail == nil { if userByEmail == nil && method != "signup" && method != "reset" {
c.ResponseError(c.T("verification:the user does not exist, please sign up first")) c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return return
} }
@ -136,7 +137,7 @@ func (c *ApiController) SendVerificationCode() {
} }
userByPhone := object.GetUserByPhone(organization.Name, dest) userByPhone := object.GetUserByPhone(organization.Name, dest)
if userByPhone == nil { if userByPhone == nil && method != "signup" && method != "reset" {
c.ResponseError(c.T("verification:the user does not exist, please sign up first")) c.ResponseError(c.T("verification:the user does not exist, please sign up first"))
return return
} }

View File

@ -256,6 +256,8 @@ func (idp *DingTalkIdProvider) isUserInOrg(unionId string) (bool, error) {
} }
if data.ErrCode == 60121 { if data.ErrCode == 60121 {
return false, fmt.Errorf("the user is not found in the organization where clientId and clientSecret belong") return false, fmt.Errorf("the user is not found in the organization where clientId and clientSecret belong")
} else if data.ErrCode != 0 {
return false, fmt.Errorf(data.ErrMessage)
} }
return true, nil return true, nil
} }

View File

@ -98,7 +98,7 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return nil return nil
} }
var gothList = []string{"Apple", "AzureAD", "Slack", "Steam"} var gothList = []string{"Apple", "AzureAD", "Slack", "Steam", "Line"}
func isGothSupport(provider string) bool { func isGothSupport(provider string) bool {
for _, value := range gothList { for _, value := range gothList {

View File

@ -156,5 +156,187 @@
"autoSync": 0, "autoSync": 0,
"lastSync": "" "lastSync": ""
} }
],
"models": [
{
"owner": "",
"name": "",
"modelText": "",
"displayName": ""
}
],
"permissions": [
{
"actions": [
""
],
"displayName": "",
"effect": "",
"isEnabled": true,
"model": "",
"name": "",
"owner": "",
"resourceType": "",
"resources": [
""
],
"roles": [
""
],
"users": [
""
]
}
],
"payments": [
{
"currency": "",
"detail": "",
"displayName": "",
"invoiceRemark": "",
"invoiceTaxId": "",
"invoiceTitle": "",
"invoiceType": "",
"invoiceUrl": "",
"message": "",
"name": "",
"organization": "",
"owner": "",
"payUrl": "",
"personEmail": "",
"personIdCard": "",
"personName": "",
"personPhone": "",
"price": 0,
"productDisplayName": "",
"productName": "",
"provider": "",
"returnUrl": "",
"state": "",
"tag": "",
"type": "",
"user": ""
}
],
"products": [
{
"currency": "",
"detail": "",
"displayName": "",
"image": "",
"name": "",
"owner": "",
"price": 0,
"providers": [
""
],
"quantity": 0,
"returnUrl": "",
"sold": 0,
"state": "",
"tag": ""
}
],
"resources": [
{
"owner": "",
"name": "",
"user": "",
"provider": "",
"application": "",
"tag": "",
"parent": "",
"fileName": "",
"fileType": "",
"fileFormat": "",
"url": "",
"description": ""
}
],
"roles": [
{
"displayName": "",
"isEnabled": true,
"name": "",
"owner": "",
"roles": [
""
],
"users": [
""
]
}
],
"syncers": [
{
"affiliationTable": "",
"avatarBaseUrl": "",
"database": "",
"databaseType": "",
"errorText": "",
"host": "",
"isEnabled": true,
"name": "",
"organization": "",
"owner": "",
"password": "",
"port": 0,
"syncInterval": 0,
"table": "",
"tableColumns": [
{
"casdoorName": "",
"isHashed": true,
"name": "",
"type": "",
"values": [
""
]
}
],
"tablePrimaryKey": "",
"type": "",
"user": ""
}
],
"tokens": [
{
"accessToken": "",
"application": "",
"code": "",
"codeChallenge": "",
"codeExpireIn": 0,
"codeIsUsed": true,
"createdTime": "",
"expiresIn": 0,
"name": "",
"organization": "",
"owner": "",
"refreshToken": "",
"scope": "",
"tokenType": "",
"user": ""
}
],
"webhooks": [
{
"contentType": "",
"events": [
""
],
"headers": [
{
"name": "",
"value": ""
}
],
"isEnabled": true,
"isUserExtended": true,
"method": "",
"name": "",
"organization": "",
"owner": "",
"url": ""
}
] ]
} }

View File

@ -16,7 +16,6 @@ package object
import ( import (
"fmt" "fmt"
"net/url"
"regexp" "regexp"
"strings" "strings"
@ -51,6 +50,7 @@ type Application struct {
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"` EnableSamlCompress bool `json:"enableSamlCompress"`
EnableWebAuthn bool `json:"enableWebAuthn"` EnableWebAuthn bool `json:"enableWebAuthn"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
@ -353,52 +353,26 @@ func (application *Application) GetId() string {
return fmt.Sprintf("%s/%s", application.Owner, application.Name) return fmt.Sprintf("%s/%s", application.Owner, application.Name)
} }
func CheckRedirectUriValid(application *Application, redirectUri string) bool { func (application *Application) IsRedirectUriValid(redirectUri string) bool {
validUri := false isValid := false
for _, tmpUri := range application.RedirectUris { for _, targetUri := range application.RedirectUris {
tmpUriRegex := regexp.MustCompile(tmpUri) targetUriRegex := regexp.MustCompile(targetUri)
if tmpUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, tmpUri) { if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
validUri = true isValid = true
break break
} }
} }
return validUri return isValid
} }
func IsAllowOrigin(origin string) bool { func IsOriginAllowed(origin string) bool {
allowOrigin := false applications := GetApplications("")
originUrl, err := url.Parse(origin) for _, application := range applications {
if err != nil { if application.IsRedirectUriValid(origin) {
return false return true
}
rows, err := adapter.Engine.Cols("redirect_uris").Rows(&Application{})
if err != nil {
panic(err)
}
application := Application{}
for rows.Next() {
err := rows.Scan(&application)
if err != nil {
panic(err)
}
for _, tmpRedirectUri := range application.RedirectUris {
u1, err := url.Parse(tmpRedirectUri)
if err != nil {
continue
}
if u1.Scheme == originUrl.Scheme && u1.Host == originUrl.Host {
allowOrigin = true
break
}
}
if allowOrigin {
break
} }
} }
return false
return allowOrigin
} }
func getApplicationMap(organization string) map[string]*Application { func getApplicationMap(organization string) map[string]*Application {

View File

@ -313,8 +313,9 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
return true, err return true, err
} }
enforcer := getEnforcer(permission) enforcer := getEnforcer(permission)
allowed, err = enforcer.Enforce(userId, application.Name, "read") if allowed, err = enforcer.Enforce(userId, application.Name, "read"); allowed {
break return allowed, err
}
} }
} }
return allowed, err return allowed, err

View File

@ -23,6 +23,15 @@ type InitData struct {
Certs []*Cert `json:"certs"` Certs []*Cert `json:"certs"`
Providers []*Provider `json:"providers"` Providers []*Provider `json:"providers"`
Ldaps []*Ldap `json:"ldaps"` Ldaps []*Ldap `json:"ldaps"`
Models []*Model `json:"models"`
Permissions []*Permission `json:"permissions"`
Payments []*Payment `json:"payments"`
Products []*Product `json:"products"`
Resources []*Resource `json:"resources"`
Roles []*Role `json:"roles"`
Syncers []*Syncer `json:"syncers"`
Tokens []*Token `json:"tokens"`
Webhooks []*Webhook `json:"webhooks"`
} }
func InitFromFile() { func InitFromFile() {
@ -46,6 +55,33 @@ func InitFromFile() {
for _, ldap := range initData.Ldaps { for _, ldap := range initData.Ldaps {
initDefinedLdap(ldap) initDefinedLdap(ldap)
} }
for _, model := range initData.Models {
initDefinedModel(model)
}
for _, permission := range initData.Permissions {
initDefinedPermission(permission)
}
for _, payment := range initData.Payments {
initDefinedPayment(payment)
}
for _, product := range initData.Products {
initDefinedProduct(product)
}
for _, resource := range initData.Resources {
initDefinedResource(resource)
}
for _, role := range initData.Roles {
initDefinedRole(role)
}
for _, syncer := range initData.Syncers {
initDefinedSyncer(syncer)
}
for _, token := range initData.Tokens {
initDefinedToken(token)
}
for _, webhook := range initData.Webhooks {
initDefinedWebhook(webhook)
}
} }
} }
@ -63,6 +99,15 @@ func readInitDataFromFile(filePath string) *InitData {
Certs: []*Cert{}, Certs: []*Cert{},
Providers: []*Provider{}, Providers: []*Provider{},
Ldaps: []*Ldap{}, Ldaps: []*Ldap{},
Models: []*Model{},
Permissions: []*Permission{},
Payments: []*Payment{},
Products: []*Product{},
Resources: []*Resource{},
Roles: []*Role{},
Syncers: []*Syncer{},
Tokens: []*Token{},
Webhooks: []*Webhook{},
} }
err := util.JsonToStruct(s, data) err := util.JsonToStruct(s, data)
if err != nil { if err != nil {
@ -89,6 +134,41 @@ func readInitDataFromFile(filePath string) *InitData {
application.RedirectUris = []string{} application.RedirectUris = []string{}
} }
} }
for _, permission := range data.Permissions {
if permission.Actions == nil {
permission.Actions = []string{}
}
if permission.Resources == nil {
permission.Resources = []string{}
}
if permission.Roles == nil {
permission.Roles = []string{}
}
if permission.Users == nil {
permission.Users = []string{}
}
}
for _, role := range data.Roles {
if role.Roles == nil {
role.Roles = []string{}
}
if role.Users == nil {
role.Users = []string{}
}
}
for _, syncer := range data.Syncers {
if syncer.TableColumns == nil {
syncer.TableColumns = []*TableColumn{}
}
}
for _, webhook := range data.Webhooks {
if webhook.Events == nil {
webhook.Events = []string{}
}
if webhook.Headers == nil {
webhook.Headers = []*Header{}
}
}
return data return data
} }
@ -174,3 +254,84 @@ func initDefinedProvider(provider *Provider) {
} }
AddProvider(provider) AddProvider(provider)
} }
func initDefinedModel(model *Model) {
existed := GetModel(model.GetId())
if existed != nil {
return
}
model.CreatedTime = util.GetCurrentTime()
AddModel(model)
}
func initDefinedPermission(permission *Permission) {
existed := GetPermission(permission.GetId())
if existed != nil {
return
}
permission.CreatedTime = util.GetCurrentTime()
AddPermission(permission)
}
func initDefinedPayment(payment *Payment) {
existed := GetPayment(payment.GetId())
if existed != nil {
return
}
payment.CreatedTime = util.GetCurrentTime()
AddPayment(payment)
}
func initDefinedProduct(product *Product) {
existed := GetProduct(product.GetId())
if existed != nil {
return
}
product.CreatedTime = util.GetCurrentTime()
AddProduct(product)
}
func initDefinedResource(resource *Resource) {
existed := GetResource(resource.GetId())
if existed != nil {
return
}
resource.CreatedTime = util.GetCurrentTime()
AddResource(resource)
}
func initDefinedRole(role *Role) {
existed := GetRole(role.GetId())
if existed != nil {
return
}
role.CreatedTime = util.GetCurrentTime()
AddRole(role)
}
func initDefinedSyncer(syncer *Syncer) {
existed := GetSyncer(syncer.GetId())
if existed != nil {
return
}
syncer.CreatedTime = util.GetCurrentTime()
AddSyncer(syncer)
}
func initDefinedToken(token *Token) {
existed := GetToken(token.GetId())
if existed != nil {
return
}
token.CreatedTime = util.GetCurrentTime()
AddToken(token)
}
func initDefinedWebhook(webhook *Webhook) {
existed := GetWebhook(webhook.GetId())
if existed != nil {
return
}
webhook.CreatedTime = util.GetCurrentTime()
AddWebhook(webhook)
}

View File

@ -41,7 +41,7 @@ func getEnforcer(permission *Permission) *casbin.Enforcer {
r = sub, obj, act r = sub, obj, act
[policy_definition] [policy_definition]
p = sub, obj, act p = sub, obj, act, "", "", permissionId
[role_definition] [role_definition]
g = _, _ g = _, _

View File

@ -27,15 +27,16 @@ type Product struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"` Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(255)" json:"detail"` Detail string `xorm:"varchar(255)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"` Description string `xorm:"varchar(100)" json:"description"`
Currency string `xorm:"varchar(100)" json:"currency"` Tag string `xorm:"varchar(100)" json:"tag"`
Price float64 `json:"price"` Currency string `xorm:"varchar(100)" json:"currency"`
Quantity int `json:"quantity"` Price float64 `json:"price"`
Sold int `json:"sold"` Quantity int `json:"quantity"`
Providers []string `xorm:"varchar(100)" json:"providers"` Sold int `json:"sold"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` Providers []string `xorm:"varchar(100)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
@ -213,6 +214,10 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
} }
func ExtendProductWithProviders(product *Product) { func ExtendProductWithProviders(product *Product) {
if product == nil {
return
}
product.ProviderObjs = []*Provider{} product.ProviderObjs = []*Provider{}
m := getProviderMap(product.Owner) m := getProviderMap(product.Owner)

View File

@ -240,8 +240,8 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
} }
// verify samlRequest // verify samlRequest
if valid := CheckRedirectUriValid(application, authnRequest.Issuer.Url); !valid { if isValid := application.IsRedirectUriValid(authnRequest.Issuer.Url); !isValid {
return "", "", fmt.Errorf("err: invalid issuer url") return "", "", fmt.Errorf("err: Issuer URI: %s doesn't exist in the allowed Redirect URI list", authnRequest.Issuer.Url)
} }
// get certificate string // get certificate string
@ -251,6 +251,11 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
_, originBackend := getOriginFromHost(host) _, originBackend := getOriginFromHost(host)
// redirect Url (Assertion Consumer Url)
if application.SamlReplyUrl != "" {
authnRequest.AssertionConsumerServiceURL = application.SamlReplyUrl
}
// build signedResponse // build signedResponse
samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris) samlResponse, _ := NewSamlResponse(user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
randomKeyStore := &X509Key{ randomKeyStore := &X509Key{

View File

@ -18,7 +18,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
@ -169,6 +168,10 @@ func GetToken(id string) *Token {
return getToken(owner, name) return getToken(owner, name)
} }
func (token *Token) GetId() string {
return fmt.Sprintf("%s/%s", token.Owner, token.Name)
}
func UpdateToken(id string, token *Token) bool { func UpdateToken(id string, token *Token) bool {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if getToken(owner, name) == nil { if getToken(owner, name) == nil {
@ -249,14 +252,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return i18n.Translate(lang, "token:Invalid client_id"), nil return i18n.Translate(lang, "token:Invalid client_id"), nil
} }
validUri := false if !application.IsRedirectUriValid(redirectUri) {
for _, tmpUri := range application.RedirectUris {
if strings.Contains(redirectUri, tmpUri) {
validUri = true
break
}
}
if !validUri {
return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application return fmt.Sprintf(i18n.Translate(lang, "token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri), application
} }

View File

@ -102,6 +102,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"`
Line string `xorm:"line varchar(100)" json:"line"`
Custom string `xorm:"custom varchar(100)" json:"custom"` Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"` WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`

View File

@ -34,7 +34,7 @@ func CorsFilter(ctx *context.Context) {
originConf := conf.GetConfigString("origin") originConf := conf.GetConfigString("origin")
if origin != "" && originConf != "" && origin != originConf { if origin != "" && originConf != "" && origin != originConf {
if object.IsAllowOrigin(origin) { if object.IsOriginAllowed(origin) {
ctx.Output.Header(headerAllowOrigin, origin) ctx.Output.Header(headerAllowOrigin, origin)
ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS") ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS")
ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization") ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")

View File

@ -54,7 +54,7 @@ class AdapterEditPage extends React.Component {
adapter: res.data, adapter: res.data,
}); });
this.getModels(this.adapter.owner); this.getModels(this.state.owner);
} }
}); });
} }

View File

@ -55,20 +55,14 @@ import CustomGithubCorner from "./CustomGithubCorner";
import * as Conf from "./Conf"; import * as Conf from "./Conf";
import * as Auth from "./auth/Auth"; import * as Auth from "./auth/Auth";
import SignupPage from "./auth/SignupPage"; import EntryPage from "./EntryPage";
import ResultPage from "./auth/ResultPage"; import ResultPage from "./auth/ResultPage";
import LoginPage from "./auth/LoginPage";
import SelfLoginPage from "./auth/SelfLoginPage";
import SelfForgetPage from "./auth/SelfForgetPage";
import ForgetPage from "./auth/ForgetPage";
import * as AuthBackend from "./auth/AuthBackend"; import * as AuthBackend from "./auth/AuthBackend";
import AuthCallback from "./auth/AuthCallback"; import AuthCallback from "./auth/AuthCallback";
import SelectLanguageBox from "./SelectLanguageBox"; import SelectLanguageBox from "./SelectLanguageBox";
import i18next from "i18next"; import i18next from "i18next";
import PromptPage from "./auth/PromptPage";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from "./auth/SamlCallback"; import SamlCallback from "./auth/SamlCallback";
import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage"; import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage"; import ModelEditPage from "./ModelEditPage";
import SystemInfo from "./SystemInfo"; import SystemInfo from "./SystemInfo";
@ -619,7 +613,7 @@ class App extends Component {
// https://www.freecodecamp.org/neyarnws/how-to-keep-your-footer-where-it-belongs-59c6aa05c59c/ // https://www.freecodecamp.org/neyarnws/how-to-keep-your-footer-where-it-belongs-59c6aa05c59c/
return ( return (
<> <React.Fragment>
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />} {!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
<Footer id="footer" style={ <Footer id="footer" style={
{ {
@ -630,63 +624,57 @@ class App extends Component {
}> }>
Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={`${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`} /></a> Powered by <a target="_blank" href="https://casdoor.org" rel="noreferrer"><img style={{paddingBottom: "3px"}} height={"20px"} alt={"Casdoor"} src={`${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`} /></a>
</Footer> </Footer>
</> </React.Fragment>
); );
} }
isDoorPages() { isDoorPages() {
return this.isEntryPages() || window.location.pathname.startsWith("/callback");
}
isEntryPages() {
return window.location.pathname.startsWith("/signup") || return window.location.pathname.startsWith("/signup") ||
window.location.pathname.startsWith("/login") || window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/callback") ||
window.location.pathname.startsWith("/prompt") ||
window.location.pathname.startsWith("/forget") || window.location.pathname.startsWith("/forget") ||
window.location.pathname.startsWith("/cas"); window.location.pathname.startsWith("/prompt") ||
window.location.pathname.startsWith("/cas") ||
window.location.pathname.startsWith("/auto-signup");
} }
renderPage() { renderPage() {
if (this.isDoorPages()) { if (this.isDoorPages()) {
return ( return (
<> <React.Fragment>
<Layout id="parent-area"> <Layout id="parent-area">
<Content style={{display: "flex", justifyContent: "center"}}> <Content style={{display: "flex", justifyContent: "center"}}>
<Switch> {
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} /> this.isEntryPages() ?
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />)} /> <EntryPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} /> :
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage account={this.state.account} {...props} />)} /> <Switch>
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signup"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> <Route exact path="/callback" component={AuthCallback} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> <Route exact path="/callback/saml" component={SamlCallback} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> <Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage account={this.state.account} type={"saml"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} /> extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} /> </Switch>
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage type={"cas"} mode={"signup"} account={this.state.account} {...props} />);}} /> }
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>
</Content> </Content>
{ {
this.renderFooter() this.renderFooter()
} }
</Layout> </Layout>
</> </React.Fragment>
); );
} }
return ( return (
<> <React.Fragment>
<FloatButton.BackTop /> <FloatButton.BackTop />
<CustomGithubCorner /> <CustomGithubCorner />
{ {
this.renderContent() this.renderContent()
} }
</> </React.Fragment>
); );
} }

View File

@ -545,6 +545,16 @@ class ApplicationEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:SAML Reply URL"), i18next.t("application:Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.application.samlReplyUrl} onChange={e => {
this.updateApplicationField("samlReplyUrl", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Enable SAML compress"), i18next.t("application:Enable SAML compress - Tooltip"))} : {Setting.getLabel(i18next.t("application:Enable SAML compress"), i18next.t("application:Enable SAML compress - Tooltip"))} :

84
web/src/EntryPage.js Normal file
View File

@ -0,0 +1,84 @@
// 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 React from "react";
import {Redirect, Route, Switch} from "react-router-dom";
import {Spin} from "antd";
import i18next from "i18next";
import * as Setting from "./Setting";
import SignupPage from "./auth/SignupPage";
import SelfLoginPage from "./auth/SelfLoginPage";
import LoginPage from "./auth/LoginPage";
import SelfForgetPage from "./auth/SelfForgetPage";
import ForgetPage from "./auth/ForgetPage";
import PromptPage from "./auth/PromptPage";
import CasLogout from "./auth/CasLogout";
class EntryPage extends React.Component {
constructor(props) {
super(props);
this.state = {
application: undefined,
};
}
renderHomeIfLoggedIn(component) {
if (this.props.account !== null && this.props.account !== undefined) {
return <Redirect to="/" />;
} else {
return component;
}
}
renderLoginIfNotLoggedIn(component) {
if (this.props.account === null) {
sessionStorage.setItem("from", window.location.pathname);
return <Redirect to="/login" />;
} else if (this.props.account === undefined) {
return null;
} else {
return component;
}
}
render() {
const onUpdateApplication = (application) => {
this.setState({
application: application,
});
};
return <div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${this.state.application?.formBackgroundUrl})`}}>
<Spin spinning={this.state.application === undefined} tip={i18next.t("login:Loading")} style={{margin: "0 auto"}} />
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/auto-signup/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signup"} onUpdateApplication={onUpdateApplication}{...props} />} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget" render={(props) => this.renderHomeIfLoggedIn(<SelfForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/forget/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<ForgetPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/prompt/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<PromptPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout {...this.props} application={this.state.application} {...props} />)} />
<Route exact path="/cas/:owner/:casApplicationName/login" render={(props) => {return (<LoginPage {...this.props} application={this.state.application} type={"cas"} mode={"signup"} {...props} />);}} />
</Switch>
</div>;
}
}
export default EntryPage;

View File

@ -13,7 +13,8 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Descriptions, Spin} from "antd"; import {Button, Descriptions, Modal, Spin} from "antd";
import {CheckCircleTwoTone} from "@ant-design/icons";
import i18next from "i18next"; import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@ -26,6 +27,7 @@ class ProductBuyPage extends React.Component {
productName: props.match?.params.productName, productName: props.match?.params.productName,
product: null, product: null,
isPlacingOrder: false, isPlacingOrder: false,
qrCodeModalProvider: null,
}; };
} }
@ -34,6 +36,10 @@ class ProductBuyPage extends React.Component {
} }
getProduct() { getProduct() {
if (this.state.productName === undefined) {
return;
}
ProductBackend.getProduct("admin", this.state.productName) ProductBackend.getProduct("admin", this.state.productName)
.then((product) => { .then((product) => {
this.setState({ this.setState({
@ -75,6 +81,13 @@ class ProductBuyPage extends React.Component {
} }
buyProduct(product, provider) { buyProduct(product, provider) {
if (provider.clientId.startsWith("http")) {
this.setState({
qrCodeModalProvider: provider,
});
return;
}
this.setState({ this.setState({
isPlacingOrder: true, isPlacingOrder: true,
}); });
@ -97,6 +110,45 @@ class ProductBuyPage extends React.Component {
}); });
} }
renderQrCodeModal() {
if (this.state.qrCodeModalProvider === undefined || this.state.qrCodeModalProvider === null) {
return null;
}
return (
<Modal title={
<div>
<CheckCircleTwoTone twoToneColor="rgb(45,120,213)" />
{" " + i18next.t("product:Please scan the QR code to pay")}
</div>
}
open={this.state.qrCodeModalProvider !== undefined && this.state.qrCodeModalProvider !== null}
onOk={() => {
Setting.goToLink(this.state.product.returnUrl);
}}
onCancel={() => {
this.setState({
qrCodeModalProvider: null,
});
}}
okText={i18next.t("product:I have completed the payment")}
cancelText={i18next.t("general:Cancel")}>
<p key={this.state.qrCodeModalProvider?.name}>
{
i18next.t("product:Please provide your username in the remark")
}
:&nbsp;&nbsp;
{
Setting.getTag("default", this.props.account.name)
}
<br />
<br />
<img src={this.state.qrCodeModalProvider?.clientId} alt={this.state.qrCodeModalProvider?.name} width={"472px"} style={{marginBottom: "20px"}} />
</p>
</Modal>
);
}
getPayButton(provider) { getPayButton(provider) {
let text = provider.type; let text = provider.type;
if (provider.type === "Alipay") { if (provider.type === "Alipay") {
@ -185,6 +237,9 @@ class ProductBuyPage extends React.Component {
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Spin> </Spin>
{
this.renderQrCodeModal()
}
</div> </div>
); );
} }

View File

@ -153,6 +153,16 @@ class ProductEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Description"), i18next.t("product:Description - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.description} onChange={e => {
this.updateProductField("description", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} : {Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} :

View File

@ -92,6 +92,7 @@ export const ResetModal = (props) => {
<CountDownInput <CountDownInput
textBefore={i18next.t("code:Code You Received")} textBefore={i18next.t("code:Code You Received")}
onChange={setCode} onChange={setCode}
method={"reset"}
onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]} onButtonClickArgs={[dest, destType, Setting.getApplicationName(application)]}
application={application} application={application}
/> />

View File

@ -32,16 +32,7 @@ class SelectLanguageBox extends React.Component {
}; };
} }
items = [ items = Setting.Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
Setting.getItem("English", "en", flagIcon("US", "English")),
Setting.getItem("简体中文", "zh", flagIcon("CN", "简体中文")),
Setting.getItem("Español", "es", flagIcon("ES", "Español")),
Setting.getItem("Français", "fr", flagIcon("FR", "Français")),
Setting.getItem("Deutsch", "de", flagIcon("DE", "Deutsch")),
Setting.getItem("日本語", "ja", flagIcon("JP", "日本語")),
Setting.getItem("한국어", "ko", flagIcon("KR", "한국어")),
Setting.getItem("Русский", "ru", flagIcon("RU", "Русский")),
];
getOrganizationLanguages(languages) { getOrganizationLanguages(languages) {
const select = []; const select = [];

View File

@ -42,12 +42,15 @@ class SelectRegionBox extends React.Component {
placeholder="Please select country/region" placeholder="Please select country/region"
onChange={(value => {this.onChange(value);})} onChange={(value => {this.onChange(value);})}
filterOption={(input, option) => filterOption={(input, option) =>
option.label.indexOf(input) >= 0 (option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
filterSort={(optionA, optionB) =>
(optionA?.label ?? "").toLowerCase().localeCompare((optionB?.label ?? "").toLowerCase())
} }
> >
{ {
Setting.CountryRegionData.map((item, index) => ( Setting.CountryRegionData.map((item, index) => (
<Option key={index} value={item.code} label={item.code} > <Option key={index} value={item.code} label={`${item.name} (${item.code})`} >
<img src={`${Setting.StaticBaseUrl}/flag-icons/${item.code}.svg`} alt={item.name} height={20} style={{marginRight: 10}} /> <img src={`${Setting.StaticBaseUrl}/flag-icons/${item.code}.svg`} alt={item.name} height={20} style={{marginRight: 10}} />
{`${item.name} (${item.code})`} {`${item.name} (${item.code})`}
</Option> </Option>

View File

@ -33,6 +33,16 @@ export const StaticBaseUrl = "https://cdn.casbin.org";
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html // https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
export const CountryRegionData = getCountryRegionData(); export const CountryRegionData = getCountryRegionData();
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"},
{label: "简体中文", key: "zh", country: "CN", alt: "简体中文"},
{label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
{label: "日本語", key: "ja", country: "JP", alt: "日本語"},
{label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"},
];
export const OtherProviderInfo = { export const OtherProviderInfo = {
SMS: { SMS: {
"Aliyun SMS": { "Aliyun SMS": {
@ -251,8 +261,10 @@ export function isValidPersonName(personName) {
} }
export function isValidIdCard(idCard) { export function isValidIdCard(idCard) {
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9X]$/; return idCard !== "";
return idCardRegex.test(idCard);
// const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9X]$/;
// return idCardRegex.test(idCard);
} }
export function isValidEmail(email) { export function isValidEmail(email) {
@ -262,34 +274,40 @@ export function isValidEmail(email) {
} }
export function isValidPhone(phone) { export function isValidPhone(phone) {
if (phone === "") { return phone !== "";
return false;
}
// https://learnku.com/articles/31543, `^s*$` filter empty email individually. // if (phone === "") {
const phoneRegex = /^\s*$|^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/; // return false;
return phoneRegex.test(phone); // }
//
// // https://learnku.com/articles/31543, `^s*$` filter empty email individually.
// const phoneRegex = /^\s*$|^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
// return phoneRegex.test(phone);
} }
export function isValidInvoiceTitle(invoiceTitle) { export function isValidInvoiceTitle(invoiceTitle) {
if (invoiceTitle === "") { return invoiceTitle !== "";
return false;
}
// https://blog.css8.cn/post/14210975.html // if (invoiceTitle === "") {
const invoiceTitleRegex = /^[()\u4e00-\u9fa5]{0,50}$/; // return false;
return invoiceTitleRegex.test(invoiceTitle); // }
//
// // https://blog.css8.cn/post/14210975.html
// const invoiceTitleRegex = /^[()\u4e00-\u9fa5]{0,50}$/;
// return invoiceTitleRegex.test(invoiceTitle);
} }
export function isValidTaxId(taxId) { export function isValidTaxId(taxId) {
// https://www.codetd.com/article/8592083 return taxId !== "";
const regArr = [/^[\da-z]{10,15}$/i, /^\d{6}[\da-z]{10,12}$/i, /^[a-z]\d{6}[\da-z]{9,11}$/i, /^[a-z]{2}\d{6}[\da-z]{8,10}$/i, /^\d{14}[\dx][\da-z]{4,5}$/i, /^\d{17}[\dx][\da-z]{1,2}$/i, /^[a-z]\d{14}[\dx][\da-z]{3,4}$/i, /^[a-z]\d{17}[\dx][\da-z]{0,1}$/i, /^[\d]{6}[\da-z]{13,14}$/i];
for (let i = 0; i < regArr.length; i++) { // // https://www.codetd.com/article/8592083
if (regArr[i].test(taxId)) { // const regArr = [/^[\da-z]{10,15}$/i, /^\d{6}[\da-z]{10,12}$/i, /^[a-z]\d{6}[\da-z]{9,11}$/i, /^[a-z]{2}\d{6}[\da-z]{8,10}$/i, /^\d{14}[\dx][\da-z]{4,5}$/i, /^\d{17}[\dx][\da-z]{1,2}$/i, /^[a-z]\d{14}[\dx][\da-z]{3,4}$/i, /^[a-z]\d{17}[\dx][\da-z]{0,1}$/i, /^[\d]{6}[\da-z]{13,14}$/i];
return true; // for (let i = 0; i < regArr.length; i++) {
} // if (regArr[i].test(taxId)) {
} // return true;
return false; // }
// }
// return false;
} }
export function isAffiliationPrompted(application) { export function isAffiliationPrompted(application) {
@ -663,6 +681,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: "Line", name: "Line"},
{id: "Custom", name: "Custom"}, {id: "Custom", name: "Custom"},
] ]
); );
@ -780,6 +799,9 @@ export function renderLoginLink(application, text) {
export function redirectToLoginPage(application, history) { export function redirectToLoginPage(application, history) {
const loginLink = getLoginLink(application); const loginLink = getLoginLink(application);
if (loginLink.indexOf("http") === 0 || loginLink.indexOf("https") === 0) {
window.location.replace(loginLink);
}
history.push(loginLink); history.push(loginLink);
} }

View File

@ -178,7 +178,7 @@ class AuthCallback extends React.Component {
render() { render() {
return ( return (
<div style={{textAlign: "center"}}> <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
{ {
(this.state.msg === null) ? ( (this.state.msg === null) ? (
<Spin size="large" tip={i18next.t("login:Signing in...")} style={{paddingTop: "10%"}} /> <Spin size="large" tip={i18next.t("login:Signing in...")} style={{paddingTop: "10%"}} />

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Spin} from "antd"; import {Card, Spin} from "antd";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
@ -39,7 +39,7 @@ class CasLogout extends React.Component {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", "Logged out successfully"); Setting.showMessage("success", "Logged out successfully");
this.props.clearAccount(); this.props.onUpdateAccount(null);
const redirectUri = res.data2; const redirectUri = res.data2;
if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") { if (redirectUri !== null && redirectUri !== undefined && redirectUri !== "") {
Setting.goToLink(redirectUri); Setting.goToLink(redirectUri);
@ -57,11 +57,13 @@ class CasLogout extends React.Component {
render() { render() {
return ( return (
<div style={{textAlign: "center"}}> <Card>
{ <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Logging out...")} style={{paddingTop: "10%"}} /> {
} <Spin size="large" tip={i18next.t("login:Logging out...")} style={{paddingTop: "10%"}} />
</div> }
</div>
</Card>
); );
} }
} }

View File

@ -34,12 +34,7 @@ class ForgetPage extends React.Component {
this.state = { this.state = {
classes: props, classes: props,
account: props.account, account: props.account,
applicationName: applicationName: props.applicationName ?? props.match.params?.applicationName,
props.applicationName !== undefined
? props.applicationName
: props.match === undefined
? null
: props.match.params.applicationName,
application: null, application: null,
msg: null, msg: null,
userId: "", userId: "",
@ -57,34 +52,36 @@ class ForgetPage extends React.Component {
}; };
} }
UNSAFE_componentWillMount() { componentDidMount() {
if (this.state.applicationName !== undefined) { if (this.getApplicationObj() === null) {
this.getApplication(); if (this.state.applicationName !== undefined) {
} else { this.getApplication();
Setting.showMessage("error", i18next.t("forget:Unknown forget type: ") + this.state.type); } else {
Setting.showMessage("error", i18next.t("forget:Unknown forget type: ") + this.state.type);
}
} }
} }
getApplication() { getApplication() {
if (this.state.applicationName === null) { if (this.state.applicationName === undefined) {
return; return;
} }
ApplicationBackend.getApplication("admin", this.state.applicationName).then( ApplicationBackend.getApplication("admin", this.state.applicationName)
(application) => { .then((application) => {
this.onUpdateApplication(application);
this.setState({ this.setState({
application: application, application: application,
}); });
} });
);
} }
getApplicationObj() { getApplicationObj() {
if (this.props.application !== undefined) { return this.props.application ?? this.state.application;
return this.props.application; }
} else {
return this.state.application; onUpdateApplication(application) {
} this.props.onUpdateApplication(application);
} }
onFormFinish(name, info, forms) { onFormFinish(name, info, forms) {
@ -143,7 +140,7 @@ class ForgetPage extends React.Component {
username: this.state.username, username: this.state.username,
name: this.state.name, name: this.state.name,
code: forms.step2.getFieldValue("emailCode"), code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.state.application?.organizationObj.phonePrefix, phonePrefix: this.getApplicationObj()?.organizationObj.phonePrefix,
type: "login", type: "login",
}, oAuthParams).then(res => { }, oAuthParams).then(res => {
if (res.status === "ok") { if (res.status === "ok") {
@ -166,10 +163,10 @@ class ForgetPage extends React.Component {
onFinish(values) { onFinish(values) {
values.username = this.state.username; values.username = this.state.username;
values.userOwner = this.state.application?.organizationObj.name; values.userOwner = this.getApplicationObj()?.organizationObj.name;
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword).then(res => { UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword).then(res => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.redirectToLoginPage(this.state.application, this.props.history); Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
} else { } else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`)); Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
} }
@ -355,13 +352,15 @@ class ForgetPage extends React.Component {
{this.state.verifyType === "email" ? ( {this.state.verifyType === "email" ? (
<CountDownInput <CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""} disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.state.application), this.state.name]} method={"forget"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(this.getApplicationObj()), this.state.name]}
application={application} application={application}
/> />
) : ( ) : (
<CountDownInput <CountDownInput
disabled={this.state.username === "" || this.state.verifyType === ""} disabled={this.state.username === "" || this.state.verifyType === ""}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.state.application), this.state.name]} method={"forget"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(this.getApplicationObj()), this.state.name]}
application={application} application={application}
/> />
)} )}
@ -490,7 +489,7 @@ class ForgetPage extends React.Component {
} }
return ( return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}> <React.Fragment>
<CustomGithubCorner /> <CustomGithubCorner />
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}> <div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<Row> <Row>
@ -548,7 +547,7 @@ class ForgetPage extends React.Component {
</Col> </Col>
</Row> </Row>
</div> </div>
</div> </React.Fragment>
); );
} }
} }

View File

@ -0,0 +1,32 @@
// Copyright 2021 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, color}) {
return <img src={`${StaticBaseUrl}/buttons/line.svg`} alt="Sign in with Line" style={{width: 24, height: 24}} />;
}
const config = {
text: "Sign in with Line",
icon: Icon,
iconFormat: name => `fa fa-${name}`,
style: {background: "#ffffff", color: "#000000"},
activeStyle: {background: "#ededee"},
};
const LineLoginButton = createButton(config);
export default LineLoginButton;

View File

@ -29,6 +29,7 @@ import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput"; import {CountDownInput} from "../common/CountDownInput";
import SelectLanguageBox from "../SelectLanguageBox"; import SelectLanguageBox from "../SelectLanguageBox";
import {CaptchaModal} from "../common/CaptchaModal"; import {CaptchaModal} from "../common/CaptchaModal";
import RedirectForm from "../common/RedirectForm";
class LoginPage extends React.Component { class LoginPage extends React.Component {
constructor(props) { constructor(props) {
@ -49,6 +50,9 @@ class LoginPage extends React.Component {
enableCaptchaModal: false, enableCaptchaModal: false,
openCaptchaModal: false, openCaptchaModal: false,
verifyCaptcha: undefined, verifyCaptcha: undefined,
samlResponse: "",
relayState: "",
redirectUrl: "",
}; };
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) { if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@ -57,16 +61,22 @@ class LoginPage extends React.Component {
} }
} }
UNSAFE_componentWillMount() { componentDidMount() {
if (this.state.type === "login" || this.state.type === "cas") { if (this.getApplicationObj() === null) {
this.getApplication(); if (this.state.type === "login" || this.state.type === "cas") {
} else if (this.state.type === "code") { this.getApplication();
this.getApplicationLogin(); } else if (this.state.type === "code") {
} else if (this.state.type === "saml") { this.getApplicationLogin();
this.getSamlApplication(); } else if (this.state.type === "saml") {
} else { this.getSamlApplication();
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`); } else {
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`);
}
} }
Setting.Countries.forEach((country) => {
new Image().src = `${Setting.StaticBaseUrl}/flag-icons/${country.country}.svg`;
});
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
@ -86,6 +96,7 @@ class LoginPage extends React.Component {
AuthBackend.getApplicationLogin(oAuthParams) AuthBackend.getApplicationLogin(oAuthParams)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.onUpdateApplication(res.data);
this.setState({ this.setState({
application: res.data, application: res.data,
}); });
@ -107,6 +118,7 @@ class LoginPage extends React.Component {
if (this.state.owner === null || this.state.owner === undefined || this.state.owner === "") { if (this.state.owner === null || this.state.owner === undefined || this.state.owner === "") {
ApplicationBackend.getApplication("admin", this.state.applicationName) ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => { .then((application) => {
this.onUpdateApplication(application);
this.setState({ this.setState({
application: application, application: application,
}); });
@ -115,6 +127,7 @@ class LoginPage extends React.Component {
OrganizationBackend.getDefaultApplication("admin", this.state.owner) OrganizationBackend.getDefaultApplication("admin", this.state.owner)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.onUpdateApplication(res.data);
this.setState({ this.setState({
application: res.data, application: res.data,
applicationName: res.data.name, applicationName: res.data.name,
@ -132,25 +145,25 @@ class LoginPage extends React.Component {
} }
ApplicationBackend.getApplication(this.state.owner, this.state.applicationName) ApplicationBackend.getApplication(this.state.owner, this.state.applicationName)
.then((application) => { .then((application) => {
this.onUpdateApplication(application);
this.setState({ this.setState({
application: application, application: application,
}); });
} });
);
} }
getApplicationObj() { getApplicationObj() {
if (this.props.application !== undefined) { return this.props.application ?? this.state.application;
return this.props.application;
} else {
return this.state.application;
}
} }
onUpdateAccount(account) { onUpdateAccount(account) {
this.props.onUpdateAccount(account); this.props.onUpdateAccount(account);
} }
onUpdateApplication(application) {
this.props.onUpdateApplication(application);
}
parseOffset(offset) { parseOffset(offset) {
if (offset === 2 || offset === 4 || Setting.inIframe() || Setting.isMobile()) { if (offset === 2 || offset === 4 || Setting.inIframe() || Setting.isMobile()) {
return "0 auto"; return "0 auto";
@ -178,10 +191,11 @@ class LoginPage extends React.Component {
if (values["samlRequest"] !== null && values["samlRequest"] !== "" && values["samlRequest"] !== undefined) { if (values["samlRequest"] !== null && values["samlRequest"] !== "" && values["samlRequest"] !== undefined) {
values["type"] = "saml"; values["type"] = "saml";
values["relayState"] = oAuthParams.relayState;
} }
if (this.state.application.organization !== null && this.state.application.organization !== undefined) { if (this.getApplicationObj()?.organization) {
values["organization"] = this.state.application.organization; values["organization"] = this.getApplicationObj().organization;
} }
} }
postCodeLoginAction(res) { postCodeLoginAction(res) {
@ -306,7 +320,15 @@ class LoginPage extends React.Component {
} else if (responseType === "saml") { } else if (responseType === "saml") {
const SAMLResponse = res.data; const SAMLResponse = res.data;
const redirectUri = res.data2; const redirectUri = res.data2;
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); if (this.state.application.assertionConsumerUrl !== "") {
this.setState({
samlResponse: res.data,
redirectUrl: res.data2,
relayState: oAuthParams.relayState,
});
} else {
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
}
} }
} else { } else {
this.setState({openCaptchaModal: false}); this.setState({openCaptchaModal: false});
@ -554,12 +576,12 @@ class LoginPage extends React.Component {
<span style={{float: "right"}}> <span style={{float: "right"}}>
{ {
!application.enableSignUp ? null : ( !application.enableSignUp ? null : (
<> <React.Fragment>
{i18next.t("login:No account?")}&nbsp; {i18next.t("login:No account?")}&nbsp;
{ {
Setting.renderSignupLink(application, i18next.t("login:sign up now")) Setting.renderSignupLink(application, i18next.t("login:sign up now"))
} }
</> </React.Fragment>
) )
} }
</span> </span>
@ -655,7 +677,7 @@ class LoginPage extends React.Component {
const rawId = assertion.rawId; const rawId = assertion.rawId;
const sig = assertion.response.signature; const sig = assertion.response.signature;
const userHandle = assertion.response.userHandle; const userHandle = assertion.response.userHandle;
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish${AuthBackend.oAuthParamsToQuery(oAuthParams)}`, { return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish?responseType=${values["type"]}`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
body: JSON.stringify({ body: JSON.stringify({
@ -719,6 +741,7 @@ class LoginPage extends React.Component {
> >
<CountDownInput <CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone} disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
method={"login"}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
application={application} application={application}
/> />
@ -754,6 +777,10 @@ class LoginPage extends React.Component {
return Util.renderMessageLarge(this, this.state.msg); return Util.renderMessageLarge(this, this.state.msg);
} }
if (this.state.samlResponse !== "") {
return <RedirectForm samlResponse={this.state.samlResponse} redirectUrl={this.state.redirectUrl} relayState={this.state.relayState} />;
}
if (application.signinHtml !== "") { if (application.signinHtml !== "") {
return ( return (
<div dangerouslySetInnerHTML={{__html: application.signinHtml}} /> <div dangerouslySetInnerHTML={{__html: application.signinHtml}} />
@ -764,14 +791,14 @@ class LoginPage extends React.Component {
if (this.props.application === undefined && !application.enablePassword && visibleOAuthProviderItems.length === 1) { if (this.props.application === undefined && !application.enablePassword && visibleOAuthProviderItems.length === 1) {
Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup")); Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup"));
return ( return (
<div style={{textAlign: "center"}}> <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
<Spin size="large" tip={i18next.t("login:Signing in...")} style={{paddingTop: "10%"}} /> <Spin size="large" tip={i18next.t("login:Signing in...")} style={{paddingTop: "10%"}} />
</div> </div>
); );
} }
return ( return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}> <React.Fragment>
<CustomGithubCorner /> <CustomGithubCorner />
<div className="login-content" style={{margin: this.parseOffset(application.formOffset)}}> <div className="login-content" style={{margin: this.parseOffset(application.formOffset)}}>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />} {Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
@ -803,7 +830,7 @@ class LoginPage extends React.Component {
</div> </div>
</div> </div>
</div> </div>
</div> </React.Fragment>
); );
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; import React from "react";
import {Button, Col, Result, Row} from "antd"; import {Button, Card, Col, Result, Row} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as UserBackend from "../backend/UserBackend"; import * as UserBackend from "../backend/UserBackend";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
@ -30,7 +30,7 @@ class PromptPage extends React.Component {
this.state = { this.state = {
classes: props, classes: props,
type: props.type, type: props.type,
applicationName: props.applicationName !== undefined ? props.applicationName : (props.match === undefined ? null : props.match.params.applicationName), applicationName: props.applicationName ?? (props.match === undefined ? null : props.match.params.applicationName),
application: null, application: null,
user: null, user: null,
}; };
@ -38,7 +38,9 @@ class PromptPage extends React.Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.getUser(); this.getUser();
this.getApplication(); if (this.getApplicationObj() === null) {
this.getApplication();
}
} }
getUser() { getUser() {
@ -59,6 +61,7 @@ class PromptPage extends React.Component {
ApplicationBackend.getApplication("admin", this.state.applicationName) ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => { .then((application) => {
this.onUpdateApplication(application);
this.setState({ this.setState({
application: application, application: application,
}); });
@ -66,11 +69,11 @@ class PromptPage extends React.Component {
} }
getApplicationObj() { getApplicationObj() {
if (this.props.application !== undefined) { return this.props.application ?? this.state.application;
return this.props.application; }
} else {
return this.state.application; onUpdateApplication(application) {
} this.props.onUpdateApplication(application);
} }
parseUserField(key, value) { parseUserField(key, value) {
@ -122,7 +125,7 @@ class PromptPage extends React.Component {
renderContent(application) { renderContent(application) {
return ( return (
<div style={{width: "400px"}}> <div style={{width: "500px"}}>
{ {
this.renderAffiliation(application) this.renderAffiliation(application)
} }
@ -247,9 +250,9 @@ class PromptPage extends React.Component {
} }
return ( return (
<Row> <div style={{display: "flex", flex: "1", justifyContent: "center"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}}> <Card>
<div style={{marginTop: "80px", marginBottom: "50px", textAlign: "center"}}> <div style={{marginTop: "30px", marginBottom: "30px", textAlign: "center"}}>
{ {
Setting.renderHelmet(application) Setting.renderHelmet(application)
} }
@ -259,16 +262,12 @@ class PromptPage extends React.Component {
{ {
this.renderContent(application) this.renderContent(application)
} }
<Row style={{margin: 10}}>
<Col span={18}>
</Col>
</Row>
<div style={{marginTop: "50px"}}> <div style={{marginTop: "50px"}}>
<Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button> <Button disabled={!Setting.isPromptAnswered(this.state.user, application)} type="primary" size="large" onClick={() => {this.submitUserEdit(true);}}>{i18next.t("code:Submit and complete")}</Button>
</div> </div>
</div> </div>
</Col> </Card>
</Row> </div>
); );
} }
} }

View File

@ -121,6 +121,10 @@ const authInfo = {
Bilibili: { Bilibili: {
endpoint: "https://passport.bilibili.com/register/pc_oauth2.html", endpoint: "https://passport.bilibili.com/register/pc_oauth2.html",
}, },
Line: {
scope: "profile%20openid%20email",
endpoint: "https://access.line.me/oauth2/v2.1/authorize",
},
}; };
export function getProviderUrl(provider) { export function getProviderUrl(provider) {
@ -256,5 +260,7 @@ export function getAuthUrl(application, provider, method) {
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`; return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`;
} else if (provider.type === "Bilibili") { } else if (provider.type === "Bilibili") {
return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code`; return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code`;
} else if (provider.type === "Line") {
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
} }
} }

View File

@ -39,6 +39,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 LineLoginButton from "./LineLoginButton";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import {getEvent} from "./Util"; import {getEvent} from "./Util";
import {Modal} from "antd"; import {Modal} from "antd";
@ -93,6 +94,8 @@ function getSigninButton(type) {
return <OktaLoginButton text={text} align={"center"} />; return <OktaLoginButton text={text} align={"center"} />;
} else if (type === "Douyin") { } else if (type === "Douyin") {
return <DouyinLoginButton text={text} align={"center"} />; return <DouyinLoginButton text={text} align={"center"} />;
} else if (type === "Line") {
return <LineLoginButton text={text} align={"center"} />;
} }
return text; return text;

View File

@ -95,7 +95,7 @@ class SamlCallback extends React.Component {
render() { render() {
return ( return (
<div style={{textAlign: "center"}}> <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
{ {
(this.state.msg === null) ? ( (this.state.msg === null) ? (
<Spin size="large" tip={i18next.t("login:Signing in...")} style={{paddingTop: "10%"}} /> <Spin size="large" tip={i18next.t("login:Signing in...")} style={{paddingTop: "10%"}} />

View File

@ -22,7 +22,6 @@ class SelfForgetPage extends React.Component {
<ForgetPage <ForgetPage
type={"forgotPassword"} type={"forgotPassword"}
applicationName={authConfig.appName} applicationName={authConfig.appName}
account={this.props.account}
{...this.props} {...this.props}
/> />
); );

View File

@ -19,7 +19,7 @@ import {authConfig} from "./Auth";
class SelfLoginPage extends React.Component { class SelfLoginPage extends React.Component {
render() { render() {
return ( return (
<LoginPage type={"login"} mode={"signin"} applicationName={authConfig.appName} account={this.props.account} {...this.props} /> <LoginPage type={"login"} mode={"signin"} applicationName={authConfig.appName} {...this.props} />
); );
} }
} }

View File

@ -65,7 +65,7 @@ class SignupPage extends React.Component {
super(props); super(props);
this.state = { this.state = {
classes: props, classes: props,
applicationName: props.match?.params.applicationName !== undefined ? props.match.params.applicationName : authConfig.appName, applicationName: props.match.params?.applicationName ?? authConfig.appName,
application: null, application: null,
email: "", email: "",
phone: "", phone: "",
@ -91,10 +91,12 @@ class SignupPage extends React.Component {
sessionStorage.setItem("signinUrl", signinUrl); sessionStorage.setItem("signinUrl", signinUrl);
} }
if (applicationName !== undefined) { if (this.getApplicationObj() === null) {
this.getApplication(applicationName); if (applicationName !== undefined) {
} else { this.getApplication(applicationName);
Setting.showMessage("error", `Unknown application name: ${applicationName}`); } else {
Setting.showMessage("error", `Unknown application name: ${applicationName}`);
}
} }
} }
@ -105,6 +107,7 @@ class SignupPage extends React.Component {
ApplicationBackend.getApplication("admin", applicationName) ApplicationBackend.getApplication("admin", applicationName)
.then((application) => { .then((application) => {
this.onUpdateApplication(application);
this.setState({ this.setState({
application: application, application: application,
}); });
@ -128,11 +131,7 @@ class SignupPage extends React.Component {
} }
getApplicationObj() { getApplicationObj() {
if (this.props.application !== undefined) { return this.props.application ?? this.state.application;
return this.props.application;
} else {
return this.state.application;
}
} }
getTermsofuseContent(url) { getTermsofuseContent(url) {
@ -149,6 +148,10 @@ class SignupPage extends React.Component {
this.props.onUpdateAccount(account); this.props.onUpdateAccount(account);
} }
onUpdateApplication(application) {
this.props.onUpdateApplication(application);
}
parseOffset(offset) { parseOffset(offset) {
if (offset === 2 || offset === 4 || Setting.inIframe() || Setting.isMobile()) { if (offset === 2 || offset === 4 || Setting.inIframe() || Setting.isMobile()) {
return "0 auto"; return "0 auto";
@ -373,6 +376,7 @@ class SignupPage extends React.Component {
> >
<CountDownInput <CountDownInput
disabled={!this.state.validEmail} disabled={!this.state.validEmail}
method={"signup"}
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.email, "email", Setting.getApplicationName(application)]}
application={application} application={application}
/> />
@ -426,6 +430,7 @@ class SignupPage extends React.Component {
> >
<CountDownInput <CountDownInput
disabled={!this.state.validPhone} disabled={!this.state.validPhone}
method={"signup"}
onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]} onButtonClickArgs={[this.state.phone, "phone", Setting.getApplicationName(application)]}
application={application} application={application}
/> />
@ -630,7 +635,7 @@ class SignupPage extends React.Component {
} }
return ( return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}> <React.Fragment>
<CustomGithubCorner /> <CustomGithubCorner />
<div className="login-content" style={{margin: this.parseOffset(application.formOffset)}}> <div className="login-content" style={{margin: this.parseOffset(application.formOffset)}}>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />} {Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
@ -655,7 +660,7 @@ class SignupPage extends React.Component {
{ {
this.renderModal() this.renderModal()
} }
</div> </React.Fragment>
); );
} }
} }

View File

@ -109,11 +109,12 @@ export function setPassword(userOwner, userName, oldPassword, newPassword) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function sendCode(checkType, checkId, checkKey, dest, type, applicationId, checkUser) { export function sendCode(checkType, checkId, checkKey, method, dest, type, applicationId, checkUser) {
const formData = new FormData(); const formData = new FormData();
formData.append("checkType", checkType); formData.append("checkType", checkType);
formData.append("checkId", checkId); formData.append("checkId", checkId);
formData.append("checkKey", checkKey); formData.append("checkKey", checkKey);
formData.append("method", method);
formData.append("dest", dest); formData.append("dest", dest);
formData.append("type", type); formData.append("type", type);
formData.append("applicationId", applicationId); formData.append("applicationId", applicationId);

View File

@ -125,5 +125,5 @@ export const CaptchaWidget = ({captchaType, subType, siteKey, clientSecret, onCh
} }
}, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]); }, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]);
return <div id="captcha"></div>; return <div id="captcha" />;
}; };

View File

@ -22,7 +22,7 @@ import {CaptchaWidget} from "./CaptchaWidget";
const {Search} = Input; const {Search} = Input;
export const CountDownInput = (props) => { export const CountDownInput = (props) => {
const {disabled, textBefore, onChange, onButtonClickArgs, application} = props; const {disabled, textBefore, onChange, onButtonClickArgs, application, method} = props;
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
const [key, setKey] = React.useState(""); const [key, setKey] = React.useState("");
const [captchaImg, setCaptchaImg] = React.useState(""); const [captchaImg, setCaptchaImg] = React.useState("");
@ -53,7 +53,7 @@ export const CountDownInput = (props) => {
const handleOk = () => { const handleOk = () => {
setVisible(false); setVisible(false);
setButtonLoading(true); setButtonLoading(true);
UserBackend.sendCode(checkType, checkId, key, ...onButtonClickArgs).then(res => { UserBackend.sendCode(checkType, checkId, key, method, ...onButtonClickArgs).then(res => {
setKey(""); setKey("");
setButtonLoading(false); setButtonLoading(false);
if (res) { if (res) {
@ -70,7 +70,7 @@ export const CountDownInput = (props) => {
const loadCaptcha = () => { const loadCaptcha = () => {
UserBackend.getCaptcha(application.owner, application.name, false).then(res => { UserBackend.getCaptcha(application.owner, application.name, false).then(res => {
if (res.type === "none") { if (res.type === "none") {
UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => { UserBackend.sendCode("none", "", "", method, ...onButtonClickArgs).then(res => {
if (res) { if (res) {
handleCountDown(60); handleCountDown(60);
} }

View File

@ -147,7 +147,7 @@ class OAuthWidget extends React.Component {
</Col> </Col>
<Col span={24 - this.props.labelSpan} > <Col span={24 - this.props.labelSpan} >
<img style={{marginRight: "10px"}} width={30} height={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" /> <img style={{marginRight: "10px"}} width={30} height={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
<span style={{width: this.props.labelSpan === 3 ? "300px" : "130px", display: (Setting.isMobile()) ? "inline" : "inline-block"}}> <span style={{width: this.props.labelSpan === 3 ? "300px" : "200px", display: (Setting.isMobile()) ? "inline" : "inline-block"}}>
{ {
linkedValue === "" ? ( linkedValue === "" ? (
"(empty)" "(empty)"

View File

@ -293,14 +293,15 @@ class PolicyTable extends React.Component {
} }
render() { render() {
return (<> return (
<Button type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.synPolicies();}}> <React.Fragment>
{i18next.t("adapter:Sync")} <Button type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.synPolicies();}}>
</Button> {i18next.t("adapter:Sync")}
{ </Button>
this.renderTable(this.state.policyLists) {
} this.renderTable(this.state.policyLists)
</> }
</React.Fragment>
); );
} }
} }

View File

@ -0,0 +1,45 @@
// 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 React, {useEffect} from "react";
import i18next from "i18next";
export const RedirectForm = (props) => {
useEffect(() => {
document.getElementById("saml").submit();
}, []);
return (
<React.Fragment>
<p>{i18next.t("login:Redirecting, please wait.")}</p>
<form id="saml" method="post" action={props.redirectUrl}>
<input
type="hidden"
name="SAMLResponse"
id="samlResponse"
value={props.samlResponse}
/>
<input
type="hidden"
name="RelayState"
id="relayState"
value={props.relayState}
/>
</form>
</React.Fragment>
);
};
export default RedirectForm;

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei", "Please select a HTML file": "Bitte wählen Sie eine HTML-Datei",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "Weiterleitungs-URL", "Redirect URL": "Weiterleitungs-URL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "Umleitungs-URLs", "Redirect URLs": "Umleitungs-URLs",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Aktualisierungs-Token läuft ab", "Refresh token expire": "Aktualisierungs-Token läuft ab",
"Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip", "Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip",
"Right": "Right", "Right": "Right",
"Rule": "Rule", "Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata", "SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip", "SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully", "SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "Weiter mit", "Continue with": "Weiter mit",
"Email or phone": "E-Mail oder Telefon", "Email or phone": "E-Mail oder Telefon",
"Forgot password?": "Passwort vergessen?", "Forgot password?": "Passwort vergessen?",
"Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"No account?": "Kein Konto?", "No account?": "Kein Konto?",
"Or sign in with another account": "Oder melden Sie sich mit einem anderen Konto an", "Or sign in with another account": "Oder melden Sie sich mit einem anderen Konto an",
@ -306,6 +309,7 @@
"Please input your password!": "Bitte geben Sie Ihr Passwort ein!", "Please input your password!": "Bitte geben Sie Ihr Passwort ein!",
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!", "Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!", "Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Anmelden", "Sign In": "Anmelden",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Mit {type} anmelden", "Sign in with {type}": "Mit {type} anmelden",
@ -432,9 +436,12 @@
"CNY": "CNY", "CNY": "CNY",
"Currency": "Currency", "Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip", "Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip", "Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"I have completed the payment": "I have completed the payment",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image - Tooltip", "Image - Tooltip": "Image - Tooltip",
"New Product": "New Product", "New Product": "New Product",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "Payment providers - Tooltip", "Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal", "Paypal": "Paypal",
"Placing order...": "Placing order...", "Placing order...": "Placing order...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "Please scan the QR code to pay",
"Price": "Price", "Price": "Price",
"Price - Tooltip": "Price - Tooltip", "Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity", "Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "Please select a HTML file", "Please select a HTML file": "Please select a HTML file",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "Redirect URL", "Redirect URL": "Redirect URL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "Redirect URLs", "Redirect URLs": "Redirect URLs",
"Redirect URLs - Tooltip": "Redirect URLs - Tooltip", "Redirect URLs - Tooltip": "Redirect URLs - Tooltip",
"Refresh token expire": "Refresh token expire", "Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip", "Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"Right": "Right", "Right": "Right",
"Rule": "Rule", "Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata", "SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip", "SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully", "SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "Continue with", "Continue with": "Continue with",
"Email or phone": "Email or phone", "Email or phone": "Email or phone",
"Forgot password?": "Forgot password?", "Forgot password?": "Forgot password?",
"Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"No account?": "No account?", "No account?": "No account?",
"Or sign in with another account": "Or sign in with another account", "Or sign in with another account": "Or sign in with another account",
@ -306,6 +309,7 @@
"Please input your password!": "Please input your password!", "Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!", "Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!", "Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Sign In", "Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Sign in with {type}", "Sign in with {type}": "Sign in with {type}",
@ -432,9 +436,12 @@
"CNY": "CNY", "CNY": "CNY",
"Currency": "Currency", "Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip", "Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip", "Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"I have completed the payment": "I have completed the payment",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image - Tooltip", "Image - Tooltip": "Image - Tooltip",
"New Product": "New Product", "New Product": "New Product",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "Payment providers - Tooltip", "Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal", "Paypal": "Paypal",
"Placing order...": "Placing order...", "Placing order...": "Placing order...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "Please scan the QR code to pay",
"Price": "Price", "Price": "Price",
"Price - Tooltip": "Price - Tooltip", "Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity", "Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "Veuillez sélectionner un fichier HTML", "Please select a HTML file": "Veuillez sélectionner un fichier HTML",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "URL de redirection", "Redirect URL": "URL de redirection",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "URL de redirection", "Redirect URLs": "URL de redirection",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Expiration du jeton d'actualisation", "Refresh token expire": "Expiration du jeton d'actualisation",
"Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle", "Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle",
"Right": "Right", "Right": "Right",
"Rule": "Rule", "Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata", "SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip", "SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully", "SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "Continuer avec", "Continue with": "Continuer avec",
"Email or phone": "Courriel ou téléphone", "Email or phone": "Courriel ou téléphone",
"Forgot password?": "Mot de passe oublié ?", "Forgot password?": "Mot de passe oublié ?",
"Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"No account?": "Pas de compte ?", "No account?": "Pas de compte ?",
"Or sign in with another account": "Ou connectez-vous avec un autre compte", "Or sign in with another account": "Ou connectez-vous avec un autre compte",
@ -306,6 +309,7 @@
"Please input your password!": "Veuillez saisir votre mot de passe !", "Please input your password!": "Veuillez saisir votre mot de passe !",
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !", "Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!", "Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Se connecter", "Sign In": "Se connecter",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Se connecter avec {type}", "Sign in with {type}": "Se connecter avec {type}",
@ -432,9 +436,12 @@
"CNY": "CNY", "CNY": "CNY",
"Currency": "Currency", "Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip", "Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip", "Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"I have completed the payment": "I have completed the payment",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image - Tooltip", "Image - Tooltip": "Image - Tooltip",
"New Product": "New Product", "New Product": "New Product",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "Payment providers - Tooltip", "Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal", "Paypal": "Paypal",
"Placing order...": "Placing order...", "Placing order...": "Placing order...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "Please scan the QR code to pay",
"Price": "Price", "Price": "Price",
"Price - Tooltip": "Price - Tooltip", "Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity", "Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "HTMLファイルを選択してください", "Please select a HTML file": "HTMLファイルを選択してください",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "リダイレクトURL", "Redirect URL": "リダイレクトURL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "リダイレクトURL", "Redirect URLs": "リダイレクトURL",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "トークンの更新の期限が切れます", "Refresh token expire": "トークンの更新の期限が切れます",
"Refresh token expire - Tooltip": "トークンの有効期限を更新する - ツールチップ", "Refresh token expire - Tooltip": "トークンの有効期限を更新する - ツールチップ",
"Right": "Right", "Right": "Right",
"Rule": "Rule", "Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata", "SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip", "SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully", "SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "次で続ける", "Continue with": "次で続ける",
"Email or phone": "Eメールまたは電話番号", "Email or phone": "Eメールまたは電話番号",
"Forgot password?": "パスワードを忘れましたか?", "Forgot password?": "パスワードを忘れましたか?",
"Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"No account?": "アカウントがありませんか?", "No account?": "アカウントがありませんか?",
"Or sign in with another account": "または別のアカウントでサインイン", "Or sign in with another account": "または別のアカウントでサインイン",
@ -306,6 +309,7 @@
"Please input your password!": "パスワードを入力してください!", "Please input your password!": "パスワードを入力してください!",
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください", "Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください",
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。", "Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "サインイン", "Sign In": "サインイン",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "{type} でサインイン", "Sign in with {type}": "{type} でサインイン",
@ -432,9 +436,12 @@
"CNY": "CNY", "CNY": "CNY",
"Currency": "Currency", "Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip", "Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip", "Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"I have completed the payment": "I have completed the payment",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image - Tooltip", "Image - Tooltip": "Image - Tooltip",
"New Product": "New Product", "New Product": "New Product",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "Payment providers - Tooltip", "Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal", "Paypal": "Paypal",
"Placing order...": "Placing order...", "Placing order...": "Placing order...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "Please scan the QR code to pay",
"Price": "Price", "Price": "Price",
"Price - Tooltip": "Price - Tooltip", "Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity", "Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "Please select a HTML file", "Please select a HTML file": "Please select a HTML file",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser",
"Redirect URL": "Redirect URL", "Redirect URL": "Redirect URL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "Redirect URLs", "Redirect URLs": "Redirect URLs",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Refresh token expire", "Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip", "Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"Right": "Right", "Right": "Right",
"Rule": "Rule", "Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata", "SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip", "SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully", "SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "Continue with", "Continue with": "Continue with",
"Email or phone": "Email or phone", "Email or phone": "Email or phone",
"Forgot password?": "Forgot password?", "Forgot password?": "Forgot password?",
"Loading": "Loading",
"Logging out...": "Logging out...", "Logging out...": "Logging out...",
"No account?": "No account?", "No account?": "No account?",
"Or sign in with another account": "Or sign in with another account", "Or sign in with another account": "Or sign in with another account",
@ -306,6 +309,7 @@
"Please input your password!": "Please input your password!", "Please input your password!": "Please input your password!",
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!", "Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
"Please input your username, Email or phone!": "Please input your username, Email or phone!", "Please input your username, Email or phone!": "Please input your username, Email or phone!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Sign In", "Sign In": "Sign In",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Sign in with {type}", "Sign in with {type}": "Sign in with {type}",
@ -432,9 +436,12 @@
"CNY": "CNY", "CNY": "CNY",
"Currency": "Currency", "Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip", "Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail", "Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip", "Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product", "Edit Product": "Edit Product",
"I have completed the payment": "I have completed the payment",
"Image": "Image", "Image": "Image",
"Image - Tooltip": "Image - Tooltip", "Image - Tooltip": "Image - Tooltip",
"New Product": "New Product", "New Product": "New Product",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "Payment providers - Tooltip", "Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal", "Paypal": "Paypal",
"Placing order...": "Placing order...", "Placing order...": "Placing order...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "Please scan the QR code to pay",
"Price": "Price", "Price": "Price",
"Price - Tooltip": "Price - Tooltip", "Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity", "Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "Пожалуйста, выберите HTML-файл", "Please select a HTML file": "Пожалуйста, выберите HTML-файл",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Ссылка на страницу успешно скопирована в буфер обмена, пожалуйста, вставьте ее в окно инкогнито или другой браузер", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "Ссылка на страницу успешно скопирована в буфер обмена, пожалуйста, вставьте ее в окно инкогнито или другой браузер",
"Redirect URL": "URL перенаправления", "Redirect URL": "URL перенаправления",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "Перенаправление URL", "Redirect URLs": "Перенаправление URL",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login", "Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Срок действия обновления токена истекает", "Refresh token expire": "Срок действия обновления токена истекает",
"Refresh token expire - Tooltip": "Срок обновления токена истекает - Подсказка", "Refresh token expire - Tooltip": "Срок обновления токена истекает - Подсказка",
"Right": "Right", "Right": "Right",
"Rule": "правило", "Rule": "правило",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "Метаданные SAML", "SAML metadata": "Метаданные SAML",
"SAML metadata - Tooltip": "Метаданные SAML - Подсказка", "SAML metadata - Tooltip": "Метаданные SAML - Подсказка",
"SAML metadata URL copied to clipboard successfully": "Адрес метаданных SAML скопирован в буфер обмена", "SAML metadata URL copied to clipboard successfully": "Адрес метаданных SAML скопирован в буфер обмена",
@ -297,6 +299,7 @@
"Continue with": "Продолжить с", "Continue with": "Продолжить с",
"Email or phone": "Электронная почта или телефон", "Email or phone": "Электронная почта или телефон",
"Forgot password?": "Забыли пароль?", "Forgot password?": "Забыли пароль?",
"Loading": "Loading",
"Logging out...": "Выход...", "Logging out...": "Выход...",
"No account?": "Нет учетной записи?", "No account?": "Нет учетной записи?",
"Or sign in with another account": "Или войти с помощью другой учетной записи", "Or sign in with another account": "Или войти с помощью другой учетной записи",
@ -306,6 +309,7 @@
"Please input your password!": "Пожалуйста, введите ваш пароль!", "Please input your password!": "Пожалуйста, введите ваш пароль!",
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!", "Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!", "Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Войти", "Sign In": "Войти",
"Sign in with WebAuthn": "Sign in with WebAuthn", "Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Войти с помощью {type}", "Sign in with {type}": "Войти с помощью {type}",
@ -432,9 +436,12 @@
"CNY": "CNY", "CNY": "CNY",
"Currency": "Currency", "Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip", "Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Сведения", "Detail": "Сведения",
"Detail - Tooltip": "Detail - Tooltip", "Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Редактирование продукта", "Edit Product": "Редактирование продукта",
"I have completed the payment": "I have completed the payment",
"Image": "Изображение", "Image": "Изображение",
"Image - Tooltip": "Image - Tooltip", "Image - Tooltip": "Image - Tooltip",
"New Product": "Новый продукт", "New Product": "Новый продукт",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "Payment providers - Tooltip", "Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "PayPal", "Paypal": "PayPal",
"Placing order...": "Placing order...", "Placing order...": "Placing order...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "Please scan the QR code to pay",
"Price": "Цена", "Price": "Цена",
"Price - Tooltip": "Price - Tooltip", "Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity", "Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"Please select a HTML file": "请选择一个HTML文件", "Please select a HTML file": "请选择一个HTML文件",
"Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "提醒页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问", "Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser": "提醒页面URL已成功复制到剪贴板请粘贴到当前浏览器的隐身模式窗口或另一个浏览器访问",
"Redirect URL": "重定向 URL", "Redirect URL": "重定向 URL",
"Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip": "回复 URL (断言使用者服务 URL, 使用POST请求返回响应) - Tooltip",
"Redirect URLs": "重定向 URLs", "Redirect URLs": "重定向 URLs",
"Redirect URLs - Tooltip": "登录成功后重定向地址列表", "Redirect URLs - Tooltip": "登录成功后重定向地址列表",
"Refresh token expire": "Refresh Token过期", "Refresh token expire": "Refresh Token过期",
"Refresh token expire - Tooltip": "Refresh Token过期时间", "Refresh token expire - Tooltip": "Refresh Token过期时间",
"Right": "居右", "Right": "居右",
"Rule": "规则", "Rule": "规则",
"SAML Reply URL": "SAML回复 URL",
"SAML metadata": "SAML元数据", "SAML metadata": "SAML元数据",
"SAML metadata - Tooltip": "SAML协议的元数据Metadata信息", "SAML metadata - Tooltip": "SAML协议的元数据Metadata信息",
"SAML metadata URL copied to clipboard successfully": "SAML元数据URL已成功复制到剪贴板", "SAML metadata URL copied to clipboard successfully": "SAML元数据URL已成功复制到剪贴板",
@ -297,6 +299,7 @@
"Continue with": "使用以下账号继续", "Continue with": "使用以下账号继续",
"Email or phone": "Email或手机号", "Email or phone": "Email或手机号",
"Forgot password?": "忘记密码?", "Forgot password?": "忘记密码?",
"Loading": "加载中",
"Logging out...": "正在退出登录...", "Logging out...": "正在退出登录...",
"No account?": "没有账号?", "No account?": "没有账号?",
"Or sign in with another account": "或者,登录其他账号", "Or sign in with another account": "或者,登录其他账号",
@ -306,6 +309,7 @@
"Please input your password!": "请输入您的密码!", "Please input your password!": "请输入您的密码!",
"Please input your password, at least 6 characters!": "请输入您的密码不少于6位", "Please input your password, at least 6 characters!": "请输入您的密码不少于6位",
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号", "Please input your username, Email or phone!": "请输入您的用户名、Email或手机号",
"Redirecting, please wait.": "正在跳转, 请稍等.",
"Sign In": "登录", "Sign In": "登录",
"Sign in with WebAuthn": "WebAuthn登录", "Sign in with WebAuthn": "WebAuthn登录",
"Sign in with {type}": "{type}登录", "Sign in with {type}": "{type}登录",
@ -432,9 +436,12 @@
"CNY": "人民币", "CNY": "人民币",
"Currency": "币种", "Currency": "币种",
"Currency - Tooltip": "币种 - 工具提示", "Currency - Tooltip": "币种 - 工具提示",
"Description": "描述",
"Description - Tooltip": "描述 - 工具提示",
"Detail": "详情", "Detail": "详情",
"Detail - Tooltip": "详情 - 工具提示", "Detail - Tooltip": "详情 - 工具提示",
"Edit Product": "编辑商品", "Edit Product": "编辑商品",
"I have completed the payment": "支付完成",
"Image": "图片", "Image": "图片",
"Image - Tooltip": "图片 - 工具提示", "Image - Tooltip": "图片 - 工具提示",
"New Product": "添加商品", "New Product": "添加商品",
@ -443,6 +450,8 @@
"Payment providers - Tooltip": "支付提供商 - 工具提示", "Payment providers - Tooltip": "支付提供商 - 工具提示",
"Paypal": "PayPal贝宝", "Paypal": "PayPal贝宝",
"Placing order...": "正在下单...", "Placing order...": "正在下单...",
"Please provide your username in the remark": "Please provide your username in the remark",
"Please scan the QR code to pay": "请扫描二维码支付",
"Price": "价格", "Price": "价格",
"Price - Tooltip": "价格 - 工具提示", "Price - Tooltip": "价格 - 工具提示",
"Quantity": "库存", "Quantity": "库存",