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
- International: https://door.casdoor.org (read-only)
- Asian mirror: https://door.casdoor.com (read-only)
- Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes)
- Read-only site: https://door.casdoor.com (any modification operation will fail)
- Writable site: https://demo.casdoor.com (original data will be restored for every 5 minutes)
## Documentation
- International: https://casdoor.org
- Asian mirror: https://casdoor.cn
https://casdoor.org
## Install

View File

@ -170,7 +170,7 @@ func (c *ApiController) GetApplicationLogin() {
}
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)
} else {
idProvider.SetHttpClient(proxy.DefaultHttpClient)

View File

@ -261,7 +261,7 @@ func (c *ApiController) TokenLogout() {
flag, application := object.DeleteTokenByAccessToken(token)
redirectUri := c.Input().Get("post_logout_redirect_uri")
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)
return
}

View File

@ -47,6 +47,7 @@ func (c *ApiController) SendVerificationCode() {
checkKey := c.Ctx.Request.Form.Get("checkKey")
checkUser := c.Ctx.Request.Form.Get("checkUser")
applicationId := c.Ctx.Request.Form.Get("applicationId")
method := c.Ctx.Request.Form.Get("method")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if destType == "" {
@ -119,7 +120,7 @@ func (c *ApiController) SendVerificationCode() {
}
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"))
return
}
@ -136,7 +137,7 @@ func (c *ApiController) SendVerificationCode() {
}
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"))
return
}

View File

@ -256,6 +256,8 @@ func (idp *DingTalkIdProvider) isUserInOrg(unionId string) (bool, error) {
}
if data.ErrCode == 60121 {
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
}

View File

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

View File

@ -156,5 +156,187 @@
"autoSync": 0,
"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 (
"fmt"
"net/url"
"regexp"
"strings"
@ -51,6 +50,7 @@ type Application struct {
EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"`
EnableWebAuthn bool `json:"enableWebAuthn"`
SamlReplyUrl string `xorm:"varchar(100)" json:"samlReplyUrl"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
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)
}
func CheckRedirectUriValid(application *Application, redirectUri string) bool {
validUri := false
for _, tmpUri := range application.RedirectUris {
tmpUriRegex := regexp.MustCompile(tmpUri)
if tmpUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, tmpUri) {
validUri = true
func (application *Application) IsRedirectUriValid(redirectUri string) bool {
isValid := false
for _, targetUri := range application.RedirectUris {
targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
isValid = true
break
}
}
return validUri
return isValid
}
func IsAllowOrigin(origin string) bool {
allowOrigin := false
originUrl, err := url.Parse(origin)
if err != nil {
return false
}
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
func IsOriginAllowed(origin string) bool {
applications := GetApplications("")
for _, application := range applications {
if application.IsRedirectUriValid(origin) {
return true
}
}
return allowOrigin
return false
}
func getApplicationMap(organization string) map[string]*Application {

View File

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

View File

@ -23,6 +23,15 @@ type InitData struct {
Certs []*Cert `json:"certs"`
Providers []*Provider `json:"providers"`
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() {
@ -46,6 +55,33 @@ func InitFromFile() {
for _, ldap := range initData.Ldaps {
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{},
Providers: []*Provider{},
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)
if err != nil {
@ -89,6 +134,41 @@ func readInitDataFromFile(filePath string) *InitData {
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
}
@ -174,3 +254,84 @@ func initDefinedProvider(provider *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
[policy_definition]
p = sub, obj, act
p = sub, obj, act, "", "", permissionId
[role_definition]
g = _, _

View File

@ -27,15 +27,16 @@ type Product struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(255)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
Image string `xorm:"varchar(100)" json:"image"`
Detail string `xorm:"varchar(255)" json:"detail"`
Description string `xorm:"varchar(100)" json:"description"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price float64 `json:"price"`
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
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) {
if product == nil {
return
}
product.ProviderObjs = []*Provider{}
m := getProviderMap(product.Owner)

View File

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

View File

@ -18,7 +18,6 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/casdoor/casdoor/i18n"
@ -169,6 +168,10 @@ func GetToken(id string) *Token {
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 {
owner, name := util.GetOwnerAndNameFromId(id)
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
}
validUri := false
for _, tmpUri := range application.RedirectUris {
if strings.Contains(redirectUri, tmpUri) {
validUri = true
break
}
}
if !validUri {
if !application.IsRedirectUriValid(redirectUri) {
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"`
Okta string `xorm:"okta varchar(100)" json:"okta"`
Douyin string `xorm:"douyin varchar(100)" json:"douyin"`
Line string `xorm:"line varchar(100)" json:"line"`
Custom string `xorm:"custom varchar(100)" json:"custom"`
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`

View File

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

View File

@ -54,7 +54,7 @@ class AdapterEditPage extends React.Component {
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 Auth from "./auth/Auth";
import SignupPage from "./auth/SignupPage";
import EntryPage from "./EntryPage";
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 AuthCallback from "./auth/AuthCallback";
import SelectLanguageBox from "./SelectLanguageBox";
import i18next from "i18next";
import PromptPage from "./auth/PromptPage";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from "./auth/SamlCallback";
import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
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/
return (
<>
<React.Fragment>
{!this.state.account ? null : <div style={{display: "none"}} id="CasdoorApplicationName" value={this.state.account.signupApplication} />}
<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>
</Footer>
</>
</React.Fragment>
);
}
isDoorPages() {
return this.isEntryPages() || window.location.pathname.startsWith("/callback");
}
isEntryPages() {
return window.location.pathname.startsWith("/signup") ||
window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/callback") ||
window.location.pathname.startsWith("/prompt") ||
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() {
if (this.isDoorPages()) {
return (
<>
<React.Fragment>
<Layout id="parent-area">
<Content style={{display: "flex", justifyContent: "center"}}>
<Switch>
<Route exact path="/signup" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} />)} />
<Route exact path="/signup/:applicationName" render={(props) => this.renderHomeIfLoggedIn(<SignupPage account={this.state.account} {...props} 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} />)} />
<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="/signup/oauth/authorize" render={(props) => <SignupPage account={this.state.account} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage account={this.state.account} type={"code"} mode={"signin"} {...props} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />} />
<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);}} />} />
<Route exact path="/cas/:owner/:casApplicationName/logout" render={(props) => this.renderHomeIfLoggedIn(<CasLogout clearAccount={() => this.setState({account: null})} {...props} />)} />
<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>
{
this.isEntryPages() ?
<EntryPage account={this.state.account} onUpdateAccount={(account) => {this.onUpdateAccount(account);}} />
:
<Switch>
<Route exact path="/callback" component={AuthCallback} />
<Route exact path="/callback/saml" component={SamlCallback} />
<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>
{
this.renderFooter()
}
</Layout>
</>
</React.Fragment>
);
}
return (
<>
<React.Fragment>
<FloatButton.BackTop />
<CustomGithubCorner />
{
this.renderContent()
}
</>
</React.Fragment>
);
}

View File

@ -545,6 +545,16 @@ class ApplicationEditPage extends React.Component {
</Select>
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
{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.
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 * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting";
@ -26,6 +27,7 @@ class ProductBuyPage extends React.Component {
productName: props.match?.params.productName,
product: null,
isPlacingOrder: false,
qrCodeModalProvider: null,
};
}
@ -34,6 +36,10 @@ class ProductBuyPage extends React.Component {
}
getProduct() {
if (this.state.productName === undefined) {
return;
}
ProductBackend.getProduct("admin", this.state.productName)
.then((product) => {
this.setState({
@ -75,6 +81,13 @@ class ProductBuyPage extends React.Component {
}
buyProduct(product, provider) {
if (provider.clientId.startsWith("http")) {
this.setState({
qrCodeModalProvider: provider,
});
return;
}
this.setState({
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) {
let text = provider.type;
if (provider.type === "Alipay") {
@ -185,6 +237,9 @@ class ProductBuyPage extends React.Component {
</Descriptions.Item>
</Descriptions>
</Spin>
{
this.renderQrCodeModal()
}
</div>
);
}

View File

@ -153,6 +153,16 @@ class ProductEditPage extends React.Component {
}} />
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Currency"), i18next.t("product:Currency - Tooltip"))} :

View File

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

View File

@ -32,16 +32,7 @@ class SelectLanguageBox extends React.Component {
};
}
items = [
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", "Русский")),
];
items = Setting.Countries.map((country) => Setting.getItem(country.label, country.key, flagIcon(country.country, country.alt)));
getOrganizationLanguages(languages) {
const select = [];

View File

@ -42,12 +42,15 @@ class SelectRegionBox extends React.Component {
placeholder="Please select country/region"
onChange={(value => {this.onChange(value);})}
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) => (
<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}} />
{`${item.name} (${item.code})`}
</Option>

View File

@ -33,6 +33,16 @@ export const StaticBaseUrl = "https://cdn.casbin.org";
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
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 = {
SMS: {
"Aliyun SMS": {
@ -251,8 +261,10 @@ export function isValidPersonName(personName) {
}
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 idCardRegex.test(idCard);
return 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) {
@ -262,34 +274,40 @@ export function isValidEmail(email) {
}
export function isValidPhone(phone) {
if (phone === "") {
return false;
}
return 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);
// if (phone === "") {
// return false;
// }
//
// // 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) {
if (invoiceTitle === "") {
return false;
}
return invoiceTitle !== "";
// https://blog.css8.cn/post/14210975.html
const invoiceTitleRegex = /^[()\u4e00-\u9fa5]{0,50}$/;
return invoiceTitleRegex.test(invoiceTitle);
// if (invoiceTitle === "") {
// return false;
// }
//
// // https://blog.css8.cn/post/14210975.html
// const invoiceTitleRegex = /^[()\u4e00-\u9fa5]{0,50}$/;
// return invoiceTitleRegex.test(invoiceTitle);
}
export function isValidTaxId(taxId) {
// https://www.codetd.com/article/8592083
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++) {
if (regArr[i].test(taxId)) {
return true;
}
}
return false;
return taxId !== "";
// // https://www.codetd.com/article/8592083
// 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++) {
// if (regArr[i].test(taxId)) {
// return true;
// }
// }
// return false;
}
export function isAffiliationPrompted(application) {
@ -663,6 +681,7 @@ export function getProviderTypeOptions(category) {
{id: "Bilibili", name: "Bilibili"},
{id: "Okta", name: "Okta"},
{id: "Douyin", name: "Douyin"},
{id: "Line", name: "Line"},
{id: "Custom", name: "Custom"},
]
);
@ -780,6 +799,9 @@ export function renderLoginLink(application, text) {
export function redirectToLoginPage(application, history) {
const loginLink = getLoginLink(application);
if (loginLink.indexOf("http") === 0 || loginLink.indexOf("https") === 0) {
window.location.replace(loginLink);
}
history.push(loginLink);
}

View File

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

View File

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

View File

@ -34,12 +34,7 @@ class ForgetPage extends React.Component {
this.state = {
classes: props,
account: props.account,
applicationName:
props.applicationName !== undefined
? props.applicationName
: props.match === undefined
? null
: props.match.params.applicationName,
applicationName: props.applicationName ?? props.match.params?.applicationName,
application: null,
msg: null,
userId: "",
@ -57,34 +52,36 @@ class ForgetPage extends React.Component {
};
}
UNSAFE_componentWillMount() {
if (this.state.applicationName !== undefined) {
this.getApplication();
} else {
Setting.showMessage("error", i18next.t("forget:Unknown forget type: ") + this.state.type);
componentDidMount() {
if (this.getApplicationObj() === null) {
if (this.state.applicationName !== undefined) {
this.getApplication();
} else {
Setting.showMessage("error", i18next.t("forget:Unknown forget type: ") + this.state.type);
}
}
}
getApplication() {
if (this.state.applicationName === null) {
if (this.state.applicationName === undefined) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName).then(
(application) => {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
this.setState({
application: application,
});
}
);
});
}
getApplicationObj() {
if (this.props.application !== undefined) {
return this.props.application;
} else {
return this.state.application;
}
return this.props.application ?? this.state.application;
}
onUpdateApplication(application) {
this.props.onUpdateApplication(application);
}
onFormFinish(name, info, forms) {
@ -143,7 +140,7 @@ class ForgetPage extends React.Component {
username: this.state.username,
name: this.state.name,
code: forms.step2.getFieldValue("emailCode"),
phonePrefix: this.state.application?.organizationObj.phonePrefix,
phonePrefix: this.getApplicationObj()?.organizationObj.phonePrefix,
type: "login",
}, oAuthParams).then(res => {
if (res.status === "ok") {
@ -166,10 +163,10 @@ class ForgetPage extends React.Component {
onFinish(values) {
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 => {
if (res.status === "ok") {
Setting.redirectToLoginPage(this.state.application, this.props.history);
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
} else {
Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
}
@ -355,13 +352,15 @@ class ForgetPage extends React.Component {
{this.state.verifyType === "email" ? (
<CountDownInput
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}
/>
) : (
<CountDownInput
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}
/>
)}
@ -490,7 +489,7 @@ class ForgetPage extends React.Component {
}
return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}>
<React.Fragment>
<CustomGithubCorner />
<div className="forget-content" style={{padding: Setting.isMobile() ? "0" : null, boxShadow: Setting.isMobile() ? "none" : null}}>
<Row>
@ -548,7 +547,7 @@ class ForgetPage extends React.Component {
</Col>
</Row>
</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 SelectLanguageBox from "../SelectLanguageBox";
import {CaptchaModal} from "../common/CaptchaModal";
import RedirectForm from "../common/RedirectForm";
class LoginPage extends React.Component {
constructor(props) {
@ -49,6 +50,9 @@ class LoginPage extends React.Component {
enableCaptchaModal: false,
openCaptchaModal: false,
verifyCaptcha: undefined,
samlResponse: "",
relayState: "",
redirectUrl: "",
};
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@ -57,16 +61,22 @@ class LoginPage extends React.Component {
}
}
UNSAFE_componentWillMount() {
if (this.state.type === "login" || this.state.type === "cas") {
this.getApplication();
} else if (this.state.type === "code") {
this.getApplicationLogin();
} else if (this.state.type === "saml") {
this.getSamlApplication();
} else {
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`);
componentDidMount() {
if (this.getApplicationObj() === null) {
if (this.state.type === "login" || this.state.type === "cas") {
this.getApplication();
} else if (this.state.type === "code") {
this.getApplicationLogin();
} else if (this.state.type === "saml") {
this.getSamlApplication();
} 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) {
@ -86,6 +96,7 @@ class LoginPage extends React.Component {
AuthBackend.getApplicationLogin(oAuthParams)
.then((res) => {
if (res.status === "ok") {
this.onUpdateApplication(res.data);
this.setState({
application: res.data,
});
@ -107,6 +118,7 @@ class LoginPage extends React.Component {
if (this.state.owner === null || this.state.owner === undefined || this.state.owner === "") {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
this.setState({
application: application,
});
@ -115,6 +127,7 @@ class LoginPage extends React.Component {
OrganizationBackend.getDefaultApplication("admin", this.state.owner)
.then((res) => {
if (res.status === "ok") {
this.onUpdateApplication(res.data);
this.setState({
application: res.data,
applicationName: res.data.name,
@ -132,25 +145,25 @@ class LoginPage extends React.Component {
}
ApplicationBackend.getApplication(this.state.owner, this.state.applicationName)
.then((application) => {
this.onUpdateApplication(application);
this.setState({
application: application,
});
}
);
});
}
getApplicationObj() {
if (this.props.application !== undefined) {
return this.props.application;
} else {
return this.state.application;
}
return this.props.application ?? this.state.application;
}
onUpdateAccount(account) {
this.props.onUpdateAccount(account);
}
onUpdateApplication(application) {
this.props.onUpdateApplication(application);
}
parseOffset(offset) {
if (offset === 2 || offset === 4 || Setting.inIframe() || Setting.isMobile()) {
return "0 auto";
@ -178,10 +191,11 @@ class LoginPage extends React.Component {
if (values["samlRequest"] !== null && values["samlRequest"] !== "" && values["samlRequest"] !== undefined) {
values["type"] = "saml";
values["relayState"] = oAuthParams.relayState;
}
if (this.state.application.organization !== null && this.state.application.organization !== undefined) {
values["organization"] = this.state.application.organization;
if (this.getApplicationObj()?.organization) {
values["organization"] = this.getApplicationObj().organization;
}
}
postCodeLoginAction(res) {
@ -306,7 +320,15 @@ class LoginPage extends React.Component {
} else if (responseType === "saml") {
const SAMLResponse = res.data;
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 {
this.setState({openCaptchaModal: false});
@ -554,12 +576,12 @@ class LoginPage extends React.Component {
<span style={{float: "right"}}>
{
!application.enableSignUp ? null : (
<>
<React.Fragment>
{i18next.t("login:No account?")}&nbsp;
{
Setting.renderSignupLink(application, i18next.t("login:sign up now"))
}
</>
</React.Fragment>
)
}
</span>
@ -655,7 +677,7 @@ class LoginPage extends React.Component {
const rawId = assertion.rawId;
const sig = assertion.response.signature;
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",
credentials: "include",
body: JSON.stringify({
@ -719,6 +741,7 @@ class LoginPage extends React.Component {
>
<CountDownInput
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
method={"login"}
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
application={application}
/>
@ -754,6 +777,10 @@ class LoginPage extends React.Component {
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 !== "") {
return (
<div dangerouslySetInnerHTML={{__html: application.signinHtml}} />
@ -764,14 +791,14 @@ class LoginPage extends React.Component {
if (this.props.application === undefined && !application.enablePassword && visibleOAuthProviderItems.length === 1) {
Setting.goToLink(Provider.getAuthUrl(application, visibleOAuthProviderItems[0].provider, "signup"));
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%"}} />
</div>
);
}
return (
<div className="loginBackground" style={{backgroundImage: Setting.inIframe() || Setting.isMobile() ? null : `url(${application.formBackgroundUrl})`}}>
<React.Fragment>
<CustomGithubCorner />
<div className="login-content" style={{margin: this.parseOffset(application.formOffset)}}>
{Setting.inIframe() || Setting.isMobile() ? null : <div dangerouslySetInnerHTML={{__html: application.formCss}} />}
@ -803,7 +830,7 @@ class LoginPage extends React.Component {
</div>
</div>
</div>
</div>
</React.Fragment>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,11 +109,12 @@ export function setPassword(userOwner, userName, oldPassword, newPassword) {
}).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();
formData.append("checkType", checkType);
formData.append("checkId", checkId);
formData.append("checkKey", checkKey);
formData.append("method", method);
formData.append("dest", dest);
formData.append("type", type);
formData.append("applicationId", applicationId);

View File

@ -125,5 +125,5 @@ export const CaptchaWidget = ({captchaType, subType, siteKey, clientSecret, onCh
}
}, [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;
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 [key, setKey] = React.useState("");
const [captchaImg, setCaptchaImg] = React.useState("");
@ -53,7 +53,7 @@ export const CountDownInput = (props) => {
const handleOk = () => {
setVisible(false);
setButtonLoading(true);
UserBackend.sendCode(checkType, checkId, key, ...onButtonClickArgs).then(res => {
UserBackend.sendCode(checkType, checkId, key, method, ...onButtonClickArgs).then(res => {
setKey("");
setButtonLoading(false);
if (res) {
@ -70,7 +70,7 @@ export const CountDownInput = (props) => {
const loadCaptcha = () => {
UserBackend.getCaptcha(application.owner, application.name, false).then(res => {
if (res.type === "none") {
UserBackend.sendCode("none", "", "", ...onButtonClickArgs).then(res => {
UserBackend.sendCode("none", "", "", method, ...onButtonClickArgs).then(res => {
if (res) {
handleCountDown(60);
}

View File

@ -147,7 +147,7 @@ class OAuthWidget extends React.Component {
</Col>
<Col span={24 - this.props.labelSpan} >
<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 === "" ? (
"(empty)"

View File

@ -293,14 +293,15 @@ class PolicyTable extends React.Component {
}
render() {
return (<>
<Button type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.synPolicies();}}>
{i18next.t("adapter:Sync")}
</Button>
{
this.renderTable(this.state.policyLists)
}
</>
return (
<React.Fragment>
<Button type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.synPolicies();}}>
{i18next.t("adapter:Sync")}
</Button>
{
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",
"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 (Assertion Consumer Service POST Binding URL) - Tooltip": "Redirect URL (Assertion Consumer Service POST Binding URL) - Tooltip",
"Redirect URLs": "Umleitungs-URLs",
"Redirect URLs - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Aktualisierungs-Token läuft ab",
"Refresh token expire - Tooltip": "Aktualisierungs-Token läuft ab - Tooltip",
"Right": "Right",
"Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "Weiter mit",
"Email or phone": "E-Mail oder Telefon",
"Forgot password?": "Passwort vergessen?",
"Loading": "Loading",
"Logging out...": "Logging out...",
"No account?": "Kein Konto?",
"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, 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!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Anmelden",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Mit {type} anmelden",
@ -432,9 +436,12 @@
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"I have completed the payment": "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",
"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",
"Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"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",
"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 - Tooltip": "Redirect URLs - Tooltip",
"Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"Right": "Right",
"Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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": "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!": "Please input your password, at least 6 characters!",
"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 {type}": "Sign in with {type}",
@ -432,9 +436,12 @@
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"I have completed the payment": "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",
"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",
"Quantity": "Quantity",

View File

@ -58,12 +58,14 @@
"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",
"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 - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Expiration du jeton d'actualisation",
"Refresh token expire - Tooltip": "Expiration du jeton d'actualisation - infobulle",
"Right": "Right",
"Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"SAML metadata URL copied to clipboard successfully": "SAML metadata URL copied to clipboard successfully",
@ -297,6 +299,7 @@
"Continue with": "Continuer avec",
"Email or phone": "Courriel ou téléphone",
"Forgot password?": "Mot de passe oublié ?",
"Loading": "Loading",
"Logging out...": "Logging out...",
"No account?": "Pas de 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, 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!",
"Redirecting, please wait.": "Redirecting, please wait.",
"Sign In": "Se connecter",
"Sign in with WebAuthn": "Sign in with WebAuthn",
"Sign in with {type}": "Se connecter avec {type}",
@ -432,9 +436,12 @@
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"I have completed the payment": "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",
"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",
"Quantity": "Quantity",

View File

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

View File

@ -58,12 +58,14 @@
"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",
"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 - Tooltip": "List of redirect addresses after successful login",
"Refresh token expire": "Refresh token expire",
"Refresh token expire - Tooltip": "Refresh token expire - Tooltip",
"Right": "Right",
"Rule": "Rule",
"SAML Reply URL": "SAML Reply URL",
"SAML metadata": "SAML metadata",
"SAML metadata - Tooltip": "SAML metadata - Tooltip",
"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": "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!": "Please input your password, at least 6 characters!",
"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 {type}": "Sign in with {type}",
@ -432,9 +436,12 @@
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Description": "Description",
"Description - Tooltip": "Description - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"I have completed the payment": "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",
"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",
"Quantity": "Quantity",

View File

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

View File

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