Compare commits

..

19 Commits

Author SHA1 Message Date
697b3e4998 feat: add implicit flow support (#520)
* feat: add implicit flow support

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: idp support in implicit flow

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-01 19:09:59 +08:00
d48d515c36 fix: Missing extendedUser in signup webhook (#522)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-03-01 18:25:48 +08:00
a5d166c35f Support language param. 2022-02-28 21:33:10 +08:00
4915963c52 fix: member No.0 bug (#516)
* fix: member No.0 bug

* Update account.go

* fix: member No.0 bug

* fix: member No.0 bug

* Update account.go
2022-02-28 19:42:11 +08:00
759a1421e5 feat: add the 'karma' prop to table User (#518)
* feature: feat : add the 'karma' prop to table User

* feat: add the 'karma' prop to table User
2022-02-28 16:25:09 +08:00
c14bf9fdab Fix bug in first name, last name checking 2022-02-28 13:17:05 +08:00
e19f07c521 Add product detail page. 2022-02-27 23:50:35 +08:00
39ab71c5db Add product pages. 2022-02-27 20:09:19 +08:00
2c97f8a8b7 feat: add two authentication flow types (#512)
* feat: add two authentication flow types

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: delete implicit method

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: use a more appropriate name

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: apply suggestion

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: remove redundant code

Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-27 14:05:07 +08:00
21392dcc14 Support user's first name and last name. 2022-02-27 14:02:52 +08:00
953d3d5bc5 Change personal to real name. 2022-02-27 13:44:44 +08:00
ddee97f544 fix: this.props.location undefined (#513)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-02-26 18:39:24 +08:00
c58a6d8725 Set enableSigninSession to false by default. 2022-02-25 23:58:13 +08:00
a5ff9549c1 Remove useless menu item. 2022-02-25 22:35:24 +08:00
fe57dcbff4 Improve translation. 2022-02-25 21:31:15 +08:00
f8c4ca0f00 feat: add cancel buttons on the edit page (#509)
* feat: add cancel buttons on the edit page

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* fix: remove warning and primary type of cancel buttons

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-02-25 18:16:02 +08:00
e738c42bd8 fix: facebook login exceptions (#508)
* Fix the exception caused by "Username" being empty when logging in with facebook

* fix: facebook login missing "Username" exception
2022-02-23 23:58:17 +08:00
cbc8c58e85 fix: oidc jwks endpoint only return default cert (#506)
Signed-off-by: Steve0x2a <stevesough@gmail.com>
2022-02-21 23:17:16 +08:00
07c90e048f Update personal name. 2022-02-21 16:01:39 +08:00
60 changed files with 2102 additions and 233 deletions

View File

@ -98,7 +98,7 @@ p, *, *, GET, /api/get-human-check, *, *
p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, *
p, *, *, *, /api/certs, *, *
p, *, *, *, /.well-known/jwks, *, *
p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
`

View File

@ -24,8 +24,10 @@ import (
)
const (
ResponseTypeLogin = "login"
ResponseTypeCode = "code"
ResponseTypeLogin = "login"
ResponseTypeCode = "code"
ResponseTypeToken = "token"
ResponseTypeIdToken = "id_token"
)
type RequestForm struct {
@ -35,6 +37,8 @@ type RequestForm struct {
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Phone string `json:"phone"`
Affiliation string `json:"affiliation"`
@ -102,7 +106,7 @@ func (c *ApiController) Signup() {
}
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", form.Organization))
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.Email, form.Phone, form.Affiliation)
msg := object.CheckUserSignup(application, organization, form.Username, form.Password, form.Name, form.FirstName, form.LastName, form.Email, form.Phone, form.Affiliation)
if msg != "" {
c.ResponseError(msg)
return
@ -145,6 +149,8 @@ func (c *ApiController) Signup() {
username = id
}
userCount := object.GetUserCount(form.Organization, "", "") + 1
user := &object.User{
Owner: form.Organization,
Name: username,
@ -167,6 +173,16 @@ func (c *ApiController) Signup() {
IsDeleted: false,
SignupApplication: application.Name,
Properties: map[string]string{},
Ranking: userCount + 1,
Karma: 0,
}
if application.GetSignupItemRule("Display name") == "First, last" {
if form.FirstName != "" || form.LastName != "" {
user.DisplayName = fmt.Sprintf("%s %s", form.FirstName, form.LastName)
user.FirstName = form.FirstName
user.LastName = form.LastName
}
}
affected := object.AddUser(user)
@ -185,6 +201,11 @@ func (c *ApiController) Signup() {
object.DisableVerificationCode(form.Email)
object.DisableVerificationCode(checkPhone)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
go object.AddRecord(record)
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
c.ResponseOk(userId)

View File

@ -38,6 +38,14 @@ func codeToResponse(code *object.Code) *Response {
return &Response{Status: "ok", Msg: "", Data: code.Code}
}
func tokenToResponse(token *object.Token) *Response {
if token.AccessToken == "" {
return &Response{Status: "error", Msg: "fail to get accessToken", Data: token.AccessToken}
}
return &Response{Status: "ok", Msg: "", Data: token.AccessToken}
}
// HandleLoggedIn ...
func (c *ApiController) HandleLoggedIn(application *object.Application, user *object.User, form *RequestForm) (resp *Response) {
userId := user.GetId()
@ -66,6 +74,15 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// The prompt page needs the user to be signed in
c.SetSessionUsername(userId)
}
} else if form.Type == ResponseTypeToken || form.Type == ResponseTypeIdToken { //implicit flow
if !object.IsGrantTypeValid(form.Type, application.GrantTypes) {
resp = &Response{Status: "error", Msg: fmt.Sprintf("error: grant_type: %s is not supported in this application", form.Type), Data: ""}
} else {
scope := c.Input().Get("scope")
token, _ := object.GetTokenByUser(application, user, scope, c.Ctx.Request.Host)
resp = tokenToResponse(token)
}
} else {
resp = &Response{Status: "error", Msg: fmt.Sprintf("Unknown response type: %s", form.Type)}
}

View File

@ -25,10 +25,10 @@ func (c *RootController) GetOidcDiscovery() {
c.ServeJSON()
}
// @Title GetOidcCert
// @Title GetJwks
// @Tag OIDC API
// @router /api/certs [get]
func (c *RootController) GetOidcCert() {
// @router /.well-known/jwks [get]
func (c *RootController) GetJwks() {
jwks, err := object.GetJsonWebKeySet()
if err != nil {
c.ResponseError(err.Error())

116
controllers/product.go Normal file
View File

@ -0,0 +1,116 @@
// 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.
package controllers
import (
"encoding/json"
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetProducts
// @Title GetProducts
// @Tag Product API
// @Description get products
// @Param owner query string true "The owner of products"
// @Success 200 {array} object.Product The Response object
// @router /get-products [get]
func (c *ApiController) GetProducts() {
owner := c.Input().Get("owner")
limit := c.Input().Get("pageSize")
page := c.Input().Get("p")
field := c.Input().Get("field")
value := c.Input().Get("value")
sortField := c.Input().Get("sortField")
sortOrder := c.Input().Get("sortOrder")
if limit == "" || page == "" {
c.Data["json"] = object.GetProducts(owner)
c.ServeJSON()
} else {
limit := util.ParseInt(limit)
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetProductCount(owner, field, value)))
products := object.GetPaginationProducts(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
c.ResponseOk(products, paginator.Nums())
}
}
// @Title GetProduct
// @Tag Product API
// @Description get product
// @Param id query string true "The id of the product"
// @Success 200 {object} object.Product The Response object
// @router /get-product [get]
func (c *ApiController) GetProduct() {
id := c.Input().Get("id")
c.Data["json"] = object.GetProduct(id)
c.ServeJSON()
}
// @Title UpdateProduct
// @Tag Product API
// @Description update product
// @Param id query string true "The id of the product"
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /update-product [post]
func (c *ApiController) UpdateProduct() {
id := c.Input().Get("id")
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.UpdateProduct(id, &product))
c.ServeJSON()
}
// @Title AddProduct
// @Tag Product API
// @Description add product
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /add-product [post]
func (c *ApiController) AddProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.AddProduct(&product))
c.ServeJSON()
}
// @Title DeleteProduct
// @Tag Product API
// @Description delete product
// @Param body body object.Product true "The details of the product"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-product [post]
func (c *ApiController) DeleteProduct() {
var product object.Product
err := json.Unmarshal(c.Ctx.Input.RequestBody, &product)
if err != nil {
panic(err)
}
c.Data["json"] = wrapActionResponse(object.DeleteProduct(&product))
c.ServeJSON()
}

View File

@ -171,12 +171,16 @@ func (c *ApiController) GetOAuthToken() {
clientSecret := c.Input().Get("client_secret")
code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope")
username := c.Input().Get("username")
password := c.Input().Get("password")
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier)
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host)
c.ServeJSON()
}

View File

@ -122,9 +122,9 @@ func (idp *FacebookIdProvider) GetToken(code string) (*oauth2.Token, error) {
//}
type FacebookUserInfo struct {
Id string `json:"id"` // The app user's App-Scoped User ID. This ID is unique to the app and cannot be used by other apps.
Name string `json:"name"` // The person's full name.
NameFormat string `json:"name_format"` // The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering.
Id string `json:"id"` // The app user's App-Scoped User ID. This ID is unique to the app and cannot be used by other apps.
Name string `json:"name"` // The person's full name.
NameFormat string `json:"name_format"` // The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering.
Picture struct { // The person's profile picture.
Data struct { // This struct is different as https://developers.facebook.com/docs/graph-api/reference/user/picture/
Height int `json:"height"`
@ -164,6 +164,7 @@ func (idp *FacebookIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
userInfo := UserInfo{
Id: facebookUserInfo.Id,
Username: facebookUserInfo.Name,
DisplayName: facebookUserInfo.Name,
Email: facebookUserInfo.Email,
AvatarUrl: facebookUserInfo.Picture.Data.Url,

View File

@ -17,7 +17,6 @@ package object
import (
"fmt"
"runtime"
"xorm.io/core"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/conf"
@ -25,6 +24,7 @@ import (
//_ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql
//_ "github.com/lib/pq" // db = postgres
"xorm.io/core"
"xorm.io/xorm"
)
@ -183,6 +183,11 @@ func (a *Adapter) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Product))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Payment))
if err != nil {
panic(err)

View File

@ -38,6 +38,7 @@ type Application struct {
EnableCodeSignin bool `json:"enableCodeSignin"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
ClientId string `xorm:"varchar(100)" json:"clientId"`

View File

@ -33,7 +33,7 @@ func init() {
reFieldWhiteList, _ = regexp.Compile(`^[A-Za-z0-9]+$`)
}
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, email string, phone string, affiliation string) string {
func CheckUserSignup(application *Application, organization *Organization, username string, password string, displayName string, firstName string, lastName string, email string, phone string, affiliation string) string {
if organization == nil {
return "organization does not exist"
}
@ -85,11 +85,19 @@ func CheckUserSignup(application *Application, organization *Organization, usern
}
if application.IsSignupItemVisible("Display name") {
if displayName == "" {
return "displayName cannot be blank"
} else if application.GetSignupItemRule("Display name") == "Personal" {
if !isValidPersonalName(displayName) {
return "displayName is not valid personal name"
if application.GetSignupItemRule("Display name") == "First, last" && (firstName != "" || lastName != "") {
if firstName == "" {
return "firstName cannot be blank"
} else if lastName == "" {
return "lastName cannot be blank"
}
} else {
if displayName == "" {
return "displayName cannot be blank"
} else if application.GetSignupItemRule("Display name") == "Real name" {
if !isValidRealName(displayName) {
return "displayName is not valid real name"
}
}
}
}

View File

@ -16,16 +16,16 @@ package object
import "regexp"
var rePersonalName *regexp.Regexp
var reRealName *regexp.Regexp
func init() {
var err error
rePersonalName, err = regexp.Compile("^[\u4E00-\u9FA5]{2,3}(?:·[\u4E00-\u9FA5]{2,3})*$")
reRealName, err = regexp.Compile("^[\u4E00-\u9FA5]{2,3}(?:·[\u4E00-\u9FA5]{2,3})*$")
if err != nil {
panic(err)
}
}
func isValidPersonalName(s string) bool {
return rePersonalName.MatchString(s)
func isValidRealName(s string) bool {
return reRealName.MatchString(s)
}

View File

@ -73,7 +73,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
JwksUri: fmt.Sprintf("%s/api/certs", originBackend),
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
ResponseTypesSupported: []string{"id_token"},
ResponseModesSupported: []string{"login", "code", "link"},
GrantTypesSupported: []string{"password", "authorization_code"},
@ -89,21 +89,22 @@ func GetOidcDiscovery(host string) OidcDiscovery {
}
func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
cert := GetDefaultCert()
certs := GetCerts("admin")
jwks := jose.JSONWebKeySet{}
//follows the protocol rfc 7517(draft)
//link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
//or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
certPemBlock := []byte(cert.PublicKey)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
for _, cert := range certs {
certPemBlock := []byte(cert.PublicKey)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, _ := x509.ParseCertificate(certDerBlock.Bytes)
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name
jwks.Keys = append(jwks.Keys, jwk)
}
var jwks jose.JSONWebKeySet
jwks.Keys = []jose.JSONWebKey{jwk}
return jwks, nil
}

130
object/product.go Normal file
View File

@ -0,0 +1,130 @@
// 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.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
type Product struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
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(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Currency string `xorm:"varchar(100)" json:"currency"`
Price int `json:"price"`
Quantity int `json:"quantity"`
Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"`
State string `xorm:"varchar(100)" json:"state"`
}
func GetProductCount(owner, field, value string) int {
session := GetSession(owner, -1, -1, field, value, "", "")
count, err := session.Count(&Product{})
if err != nil {
panic(err)
}
return int(count)
}
func GetProducts(owner string) []*Product {
products := []*Product{}
err := adapter.Engine.Desc("created_time").Find(&products, &Product{Owner: owner})
if err != nil {
panic(err)
}
return products
}
func GetPaginationProducts(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Product {
products := []*Product{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&products)
if err != nil {
panic(err)
}
return products
}
func getProduct(owner string, name string) *Product {
if owner == "" || name == "" {
return nil
}
product := Product{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&product)
if err != nil {
panic(err)
}
if existed {
return &product
} else {
return nil
}
}
func GetProduct(id string) *Product {
owner, name := util.GetOwnerAndNameFromId(id)
return getProduct(owner, name)
}
func UpdateProduct(id string, product *Product) bool {
owner, name := util.GetOwnerAndNameFromId(id)
if getProduct(owner, name) == nil {
return false
}
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(product)
if err != nil {
panic(err)
}
return affected != 0
}
func AddProduct(product *Product) bool {
affected, err := adapter.Engine.Insert(product)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteProduct(product *Product) bool {
affected, err := adapter.Engine.ID(core.PK{product.Owner, product.Name}).Delete(&Product{})
if err != nil {
panic(err)
}
return affected != 0
}
func (product *Product) GetId() string {
return fmt.Sprintf("%s/%s", product.Owner, product.Name)
}

View File

@ -17,6 +17,7 @@ package object
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
@ -179,8 +180,8 @@ func GetTokenByAccessToken(accessToken string) *Token {
}
func CheckOAuthLogin(clientId string, responseType string, redirectUri string, scope string, state string) (string, *Application) {
if responseType != "code" {
return "response_type should be \"code\"", nil
if responseType != "code" && responseType != "token" && responseType != "id_token" {
return fmt.Sprintf("error: grant_type: %s is not supported in this application", responseType), nil
}
application := GetApplicationByClientId(clientId)
@ -261,7 +262,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
}
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string) *TokenWrapper {
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string) *TokenWrapper {
application := GetApplicationByClientId(clientId)
if application == nil {
return &TokenWrapper{
@ -272,75 +273,30 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
}
}
if grantType != "authorization_code" {
//Check if grantType is allowed in the current application
if !IsGrantTypeValid(grantType, application.GrantTypes) {
return &TokenWrapper{
AccessToken: "error: grant_type should be \"authorization_code\"",
AccessToken: fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType),
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if code == "" {
return &TokenWrapper{
AccessToken: "error: authorization code should not be empty",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
var token *Token
var err error
switch grantType {
case "authorization_code": // Authorization Code Grant
token, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
case "password": // Resource Owner Password Credentials Grant
token, err = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant
token, err = GetClientCredentialsToken(application, clientSecret, scope, host)
}
token := getTokenByCode(code)
if token == nil {
if err != nil {
return &TokenWrapper{
AccessToken: "error: invalid authorization code",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if application.Name != token.Application {
return &TokenWrapper{
AccessToken: "error: the token is for wrong application (client_id)",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if application.ClientSecret != clientSecret {
return &TokenWrapper{
AccessToken: "error: invalid client_secret",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
return &TokenWrapper{
AccessToken: "error: incorrect code_verifier",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if token.CodeIsUsed {
// anti replay attacks
return &TokenWrapper{
AccessToken: "error: authorization code has been used",
TokenType: "",
ExpiresIn: 0,
Scope: "",
}
}
if time.Now().Unix() > token.CodeExpireIn {
// code must be used within 5 minutes
return &TokenWrapper{
AccessToken: "error: authorization code has expired",
AccessToken: err.Error(),
TokenType: "",
ExpiresIn: 0,
Scope: "",
@ -459,3 +415,140 @@ func pkceChallenge(verifier string) string {
challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
return challenge
}
// Check if grantType is allowed in the current application
// authorization_code is allowed by default
func IsGrantTypeValid(method string, grantTypes []string) bool {
if method == "authorization_code" {
return true
}
for _, m := range grantTypes {
if m == method {
return true
}
}
return false
}
// Authorization code flow
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, error) {
if code == "" {
return nil, errors.New("error: authorization code should not be empty")
}
token := getTokenByCode(code)
if token == nil {
return nil, errors.New("error: invalid authorization code")
}
if token.CodeIsUsed {
// anti replay attacks
return nil, errors.New("error: authorization code has been used")
}
if application.ClientSecret != clientSecret {
return nil, errors.New("error: invalid client_secret")
}
if application.Name != token.Application {
return nil, errors.New("error: the token is for wrong application (client_id)")
}
if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
return nil, errors.New("error: incorrect code_verifier")
}
if time.Now().Unix() > token.CodeExpireIn {
// code must be used within 5 minutes
return nil, errors.New("error: authorization code has expired")
}
return token, nil
}
// Resource Owner Password Credentials flow
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, error) {
user := getUser(application.Organization, username)
if user == nil {
return nil, errors.New("error: the user does not exist")
}
if user.Password != password {
return nil, errors.New("error: invalid username or password")
}
if user.IsForbidden {
return nil, errors.New("error: the user is forbidden to sign in, please contact the administrator")
}
accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: user.Owner,
User: user.Name,
Code: util.GenerateClientId(),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeIsUsed: true,
}
AddToken(token)
return token, nil
}
// Client Credentials flow
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, error) {
if application.ClientSecret != clientSecret {
return nil, errors.New("error: invalid client_secret")
}
nullUser := &User{
Name: fmt.Sprintf("app/%s", application.Name),
}
accessToken, _, err := generateJwtToken(application, nullUser, "", scope, host)
if err != nil {
return nil, err
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: application.Organization,
User: nullUser.Name,
Code: util.GenerateClientId(),
AccessToken: accessToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeIsUsed: true,
}
AddToken(token)
return token, nil
}
// Implicit flow
func GetTokenByUser(application *Application, user *User, scope string, host string) (*Token, error) {
accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return nil, err
}
token := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: user.Owner,
User: user.Name,
Code: util.GenerateClientId(),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
CodeIsUsed: true,
}
AddToken(token)
return token, nil
}

View File

@ -34,6 +34,8 @@ type User struct {
Password string `xorm:"varchar(100)" json:"password"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
FirstName string `xorm:"varchar(100)" json:"firstName"`
LastName string `xorm:"varchar(100)" json:"lastName"`
Avatar string `xorm:"varchar(500)" json:"avatar"`
PermanentAvatar string `xorm:"varchar(500)" json:"permanentAvatar"`
Email string `xorm:"varchar(100) index" json:"email"`
@ -53,6 +55,7 @@ type User struct {
Birthday string `xorm:"varchar(100)" json:"birthday"`
Education string `xorm:"varchar(100)" json:"education"`
Score int `json:"score"`
Karma int `json:"karma"`
Ranking int `json:"ranking"`
IsDefaultAvatar bool `json:"isDefaultAvatar"`
IsOnline bool `json:"isOnline"`

View File

@ -62,7 +62,7 @@ func AutoSigninFilter(ctx *context.Context) {
// "/page?username=abc&password=123"
userId = ctx.Input.Query("username")
password := ctx.Input.Query("password")
if userId != "" && password != "" {
if userId != "" && password != "" && ctx.Input.Query("grant_type") == "" {
owner, name := util.GetOwnerAndNameFromId(userId)
_, msg := object.CheckUserPassword(owner, name, password)
if msg != "" {

View File

@ -54,7 +54,7 @@ func getUserByClientIdSecret(ctx *context.Context) string {
}
func RecordMessage(ctx *context.Context) {
if ctx.Request.URL.Path == "/api/login" {
if ctx.Request.URL.Path == "/api/login" || ctx.Request.URL.Path == "/api/signup" {
return
}

View File

@ -149,6 +149,12 @@ func initAPI() {
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct")
beego.Router("/api/update-product", &controllers.ApiController{}, "POST:UpdateProduct")
beego.Router("/api/add-product", &controllers.ApiController{}, "POST:AddProduct")
beego.Router("/api/delete-product", &controllers.ApiController{}, "POST:DeleteProduct")
beego.Router("/api/get-payments", &controllers.ApiController{}, "GET:GetPayments")
beego.Router("/api/get-payment", &controllers.ApiController{}, "GET:GetPayment")
beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")
@ -159,5 +165,5 @@ func initAPI() {
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
beego.Router("/api/certs", &controllers.RootController{}, "*:GetOidcCert")
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")
}

View File

@ -10,6 +10,14 @@
},
"basePath": "/",
"paths": {
"/.well-known/jwks": {
"get": {
"tags": [
"OIDC API"
],
"operationId": "RootController.GetJwks"
}
},
"/.well-known/openid-configuration": {
"get": {
"tags": [
@ -442,14 +450,6 @@
}
}
},
"/api/certs": {
"get": {
"tags": [
"OIDC API"
],
"operationId": "RootController.GetOidcCert"
}
},
"/api/check-ldap-users-exist": {
"post": {
"tags": [
@ -2476,11 +2476,11 @@
}
},
"definitions": {
"1867.0xc0003b2ea0.false": {
"1867.0xc00029b560.false": {
"title": "false",
"type": "object"
},
"1901.0xc0003b2ed0.false": {
"1901.0xc00029b590.false": {
"title": "false",
"type": "object"
},
@ -2497,10 +2497,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/1867.0xc0003b2ea0.false"
"$ref": "#/definitions/1867.0xc00029b560.false"
},
"data2": {
"$ref": "#/definitions/1901.0xc0003b2ed0.false"
"$ref": "#/definitions/1901.0xc00029b590.false"
},
"msg": {
"type": "string"
@ -2521,10 +2521,10 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/1867.0xc0003b2ea0.false"
"$ref": "#/definitions/1867.0xc00029b560.false"
},
"data2": {
"$ref": "#/definitions/1901.0xc0003b2ed0.false"
"$ref": "#/definitions/1901.0xc00029b590.false"
},
"msg": {
"type": "string"
@ -3213,6 +3213,9 @@
"type": "string"
}
},
"adfs": {
"type": "string"
},
"affiliation": {
"type": "string"
},

View File

@ -7,6 +7,11 @@ info:
email: admin@casbin.org
basePath: /
paths:
/.well-known/jwks:
get:
tags:
- OIDC API
operationId: RootController.GetJwks
/.well-known/openid-configuration:
get:
tags:
@ -286,11 +291,6 @@ paths:
description: object
schema:
$ref: '#/definitions/Response'
/api/certs:
get:
tags:
- OIDC API
operationId: RootController.GetOidcCert
/api/check-ldap-users-exist:
post:
tags:
@ -1620,10 +1620,10 @@ paths:
schema:
$ref: '#/definitions/object.Userinfo'
definitions:
1867.0xc0003b2ea0.false:
1867.0xc00029b560.false:
title: "false"
type: object
1901.0xc0003b2ed0.false:
1901.0xc00029b590.false:
title: "false"
type: object
RequestForm:
@ -1637,9 +1637,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/1867.0xc0003b2ea0.false'
$ref: '#/definitions/1867.0xc00029b560.false'
data2:
$ref: '#/definitions/1901.0xc0003b2ed0.false'
$ref: '#/definitions/1901.0xc00029b590.false'
msg:
type: string
name:
@ -1653,9 +1653,9 @@ definitions:
type: object
properties:
data:
$ref: '#/definitions/1867.0xc0003b2ea0.false'
$ref: '#/definitions/1867.0xc00029b560.false'
data2:
$ref: '#/definitions/1901.0xc0003b2ed0.false'
$ref: '#/definitions/1901.0xc00029b590.false'
msg:
type: string
name:
@ -2118,6 +2118,8 @@ definitions:
type: array
items:
type: string
adfs:
type: string
affiliation:
type: string
apple:

View File

@ -43,6 +43,9 @@ import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import ProductListPage from "./ProductListPage";
import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import AccountPage from "./account/AccountPage";
@ -128,6 +131,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/syncers' });
} else if (uri.includes('/certs')) {
this.setState({ selectedMenuKey: '/certs' });
} else if (uri.includes('/products')) {
this.setState({ selectedMenuKey: '/products' });
} else if (uri.includes('/payments')) {
this.setState({ selectedMenuKey: '/payments' });
} else if (uri.includes('/signup')) {
@ -141,16 +146,14 @@ class App extends Component {
}
}
getAccessTokenParam() {
getAccessTokenParam(params) {
// "/page?access_token=123"
const params = new URLSearchParams(this.props.location.search);
const accessToken = params.get("access_token");
return accessToken === null ? "" : `?accessToken=${accessToken}`;
}
getCredentialParams() {
getCredentialParams(params) {
// "/page?username=abc&password=123"
const params = new URLSearchParams(this.props.location.search);
if (params.get("username") === null || params.get("password") === null) {
return "";
}
@ -158,8 +161,17 @@ class App extends Component {
}
getUrlWithoutQuery() {
// eslint-disable-next-line no-restricted-globals
return location.toString().replace(location.search, "");
return window.location.toString().replace(window.location.search, "");
}
getLanguageParam(params) {
// "/page?language=en"
const language = params.get("language");
if (language !== null) {
Setting.setLanguage(language);
return `language=${language}`;
}
return "";
}
setLanguage(account) {
@ -170,13 +182,23 @@ class App extends Component {
}
getAccount() {
let query = this.getAccessTokenParam();
const params = new URLSearchParams(this.props.location.search);
let query = this.getAccessTokenParam(params);
if (query === "") {
query = this.getCredentialParams();
query = this.getCredentialParams(params);
}
const query2 = this.getLanguageParam(params);
if (query2 !== "") {
const url = window.location.toString().replace(new RegExp(`[?&]${query2}`), "");
window.history.replaceState({}, document.title, url);
}
if (query !== "") {
window.history.replaceState({}, document.title, this.getUrlWithoutQuery());
}
AuthBackend.getAccount(query)
.then((res) => {
let account = null;
@ -412,6 +434,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/products">
<Link to="/products">
{i18next.t("general:Products")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/payments">
<Link to="/payments">
@ -490,6 +519,9 @@ class App extends Component {
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)}/>
<Route exact path="/certs/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)}/>
<Route exact path="/products/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/products/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)}/>
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>

View File

@ -47,6 +47,7 @@ class ApplicationEditPage extends React.Component {
certs: [],
providers: [],
uploading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -60,6 +61,9 @@ class ApplicationEditPage extends React.Component {
getApplication() {
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => {
if (application.grantTypes === null || application.grantTypes.length === 0) {
application.grantTypes = ["authorization_code"];
}
this.setState({
application: application,
});
@ -134,9 +138,10 @@ class ApplicationEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("application:Edit Application")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("application:New Application") : i18next.t("application:Edit Application")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -161,12 +166,12 @@ class ApplicationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Logo", i18next.t("general:Logo - Tooltip"))} :
{Setting.getLabel("general:Logo", i18next.t("general:Logo - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.application.logo} onChange={e => {
@ -433,6 +438,28 @@ class ApplicationEditPage extends React.Component {
</Popover>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Grant types"), i18next.t("application:Grant types - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}}
value={this.state.application.grantTypes}
onChange={(value => {
this.updateApplicationField('grantTypes', value);
})} >
{
[
{id: "authorization_code", name: "Authorization Code"},
{id: "password", name: "Password"},
{id: "client_credentials", name: "Client Credentials"},
{id: "token", name: "Token"},
{id: "id_token",name:"ID Token"},
].map((item, index)=><Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :
@ -492,13 +519,13 @@ class ApplicationEditPage extends React.Component {
if (!Setting.isMobile()) {
return (
<React.Fragment>
<Col span={11} style={{display:'flex',flexDirection:'column'}}>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Col span={11} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<br/>
<br/>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888" ,alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto'}}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@ -508,13 +535,13 @@ class ApplicationEditPage extends React.Component {
}
</div>
</Col>
<Col span={11} style={{display:'flex',flexDirection:'column'}}>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signInUrl}>
<Col span={11} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<br/>
<br/>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888",alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto' }}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
</div>
</Col>
@ -523,11 +550,11 @@ class ApplicationEditPage extends React.Component {
} else{
return(
<React.Fragment>
<Col span={24} style={{display:'flex',flexDirection:'column'}}>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Col span={24} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<div style={{marginBottom:'10px', width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888" ,alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto'}}>
<div style={{marginBottom:"10px", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
@ -536,10 +563,10 @@ class ApplicationEditPage extends React.Component {
)
}
</div>
<a style={{marginBottom: '10px',display:'flex'}} target="_blank" rel="noreferrer" href={signInUrl}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888",alignItems:'center',overflow:'auto',flexDirection:'column',flex:'auto' }}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
</div>
</Col>
@ -553,13 +580,13 @@ class ApplicationEditPage extends React.Component {
return (
<React.Fragment>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:'flex',flexDirection:'column',flex:'auto'}} >
<a style={{marginBottom: '10px'}} target="_blank" rel="noreferrer" href={promptUrl}>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:"flex", flexDirection: "column", flex: "auto"}} >
<a style={{marginBottom: "10px"}} target="_blank" rel="noreferrer" href={promptUrl}>
<Button type="primary">{i18next.t("application:Test prompt page..")}</Button>
</a>
<br style={(Setting.isMobile()) ? {display:'none'} : {}} />
<br style={(Setting.isMobile()) ? {display:'none'} : {}} />
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888",flexDirection:'column',flex:'auto'}}>
<br style={(Setting.isMobile()) ? {display: "none"} : {}} />
<br style={(Setting.isMobile()) ? {display: "none"} : {}} />
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
<PromptPage application={this.state.application} account={this.props.account} />
</div>
</Col>
@ -592,6 +619,16 @@ class ApplicationEditPage extends React.Component {
});
}
deleteApplication() {
ApplicationBackend.deleteApplication(this.state.application)
.then(() => {
this.props.history.push(`/applications`);
})
.catch(error => {
Setting.showMessage("error", `Application failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -601,6 +638,7 @@ class ApplicationEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitApplicationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitApplicationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteApplication()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -34,7 +34,7 @@ class ApplicationListPage extends BaseListPage {
logo: "https://cdn.casdoor.com/logo/casdoor-logo_1185x256.png",
enablePassword: true,
enableSignUp: true,
enableSigninSession: true,
enableSigninSession: false,
enableCodeSignin: false,
providers: [],
signupItems: [
@ -58,8 +58,7 @@ class ApplicationListPage extends BaseListPage {
const newApplication = this.newApplication();
ApplicationBackend.addApplication(newApplication)
.then((res) => {
Setting.showMessage("success", `Application added successfully`);
this.props.history.push(`/applications/${newApplication.name}`);
this.props.history.push({pathname: `/applications/${newApplication.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -30,6 +30,7 @@ class CertEditPage extends React.Component {
classes: props,
certName: props.match.params.certName,
cert: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -67,9 +68,10 @@ class CertEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("cert:New Cert") : i18next.t("cert:Edit Cert")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -236,6 +238,16 @@ class CertEditPage extends React.Component {
});
}
deleteCert() {
CertBackend.deleteCert(this.state.cert)
.then(() => {
this.props.history.push(`/certs`);
})
.catch(error => {
Setting.showMessage("error", `Cert failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -245,6 +257,7 @@ class CertEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitCertEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitCertEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteCert()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -44,8 +44,7 @@ class CertListPage extends BaseListPage {
const newCert = this.newCert();
CertBackend.addCert(newCert)
.then((res) => {
Setting.showMessage("success", `Cert added successfully`);
this.props.history.push(`/certs/${newCert.name}`);
this.props.history.push({pathname: `/certs/${newCert.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -31,6 +31,7 @@ class OrganizationEditPage extends React.Component {
organizationName: props.match.params.organizationName,
organization: null,
ldaps: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -84,9 +85,10 @@ class OrganizationEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("organization:Edit Organization")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("organization:New Organization") : i18next.t("organization:Edit Organization")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -111,12 +113,12 @@ class OrganizationEditPage extends React.Component {
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("Favicon", i18next.t("general:Favicon - Tooltip"))} :
{Setting.getLabel("general:Favicon", i18next.t("general:Favicon - Tooltip"))} :
</Col>
<Col span={22} >
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.organization.favicon} onChange={e => {
@ -186,7 +188,7 @@ class OrganizationEditPage extends React.Component {
<Col span={22} >
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
URL:
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.organization.defaultAvatar} onChange={e => {
@ -269,6 +271,16 @@ class OrganizationEditPage extends React.Component {
});
}
deleteOrganization() {
OrganizationBackend.deleteOrganization(this.state.organization)
.then(() => {
this.props.history.push(`/organizations`);
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
@ -278,6 +290,7 @@ class OrganizationEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitOrganizationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitOrganizationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteOrganization()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -45,8 +45,7 @@ class OrganizationListPage extends BaseListPage {
const newOrganization = this.newOrganization();
OrganizationBackend.addOrganization(newOrganization)
.then((res) => {
Setting.showMessage("success", `Organization added successfully`);
this.props.history.push(`/organizations/${newOrganization.name}`);
this.props.history.push({pathname: `/organizations/${newOrganization.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -13,15 +13,10 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
import {Button, Card, Col, Input, Row} from 'antd';
import * as PaymentBackend from "./backend/PaymentBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend";
const { Option } = Select;
class PaymentEditPage extends React.Component {
constructor(props) {
@ -31,6 +26,7 @@ class PaymentEditPage extends React.Component {
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
paymentName: props.match.params.paymentName,
payment: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -68,9 +64,10 @@ class PaymentEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("payment:Edit Payment")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("payment:New Payment") : i18next.t("payment:Edit Payment")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -103,16 +100,6 @@ class PaymentEditPage extends React.Component {
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.payment.name} onChange={e => {
// this.updatePaymentField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} :
@ -192,6 +179,16 @@ class PaymentEditPage extends React.Component {
});
}
deletePayment() {
PaymentBackend.deletePayment(this.state.payment)
.then(() => {
this.props.history.push(`/payments`);
})
.catch(error => {
Setting.showMessage("error", `Payment failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -201,6 +198,7 @@ class PaymentEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deletePayment()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Switch, Table} from 'antd';
import {Button, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as PaymentBackend from "./backend/PaymentBackend";
@ -45,8 +45,7 @@ class PaymentListPage extends BaseListPage {
const newPayment = this.newPayment();
PaymentBackend.addPayment(newPayment)
.then((res) => {
Setting.showMessage("success", `Payment added successfully`);
this.props.history.push(`/payments/${newPayment.name}`);
this.props.history.push({pathname: `/payments/${newPayment.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -34,6 +34,7 @@ class PermissionEditPage extends React.Component {
organizations: [],
users: [],
roles: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -102,9 +103,10 @@ class PermissionEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("permission:New Permission") : i18next.t("permission:Edit Permission")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -258,6 +260,16 @@ class PermissionEditPage extends React.Component {
});
}
deletePermission() {
PermissionBackend.deletePermission(this.state.permission)
.then(() => {
this.props.history.push(`/permissions`);
})
.catch(error => {
Setting.showMessage("error", `Permission failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -267,6 +279,7 @@ class PermissionEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitPermissionEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPermissionEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deletePermission()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -43,8 +43,7 @@ class PermissionListPage extends BaseListPage {
const newPermission = this.newPermission();
PermissionBackend.addPermission(newPermission)
.then((res) => {
Setting.showMessage("success", `Permission added successfully`);
this.props.history.push(`/permissions/${newPermission.owner}/${newPermission.name}`);
this.props.history.push({pathname: `/permissions/${newPermission.owner}/${newPermission.name}`, mode: "add"});
}
)
.catch(error => {

196
web/src/ProductBuyPage.js Normal file
View File

@ -0,0 +1,196 @@
// 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 {Button, Descriptions} from "antd";
import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend";
import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider";
class ProductBuyPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
productName: props.match?.params.productName,
product: null,
providers: [],
};
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentProviders();
}
getProduct() {
ProductBackend.getProduct("admin", this.state.productName)
.then((product) => {
this.setState({
product: product,
});
});
}
getPaymentProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res.filter(provider => provider.category === "Payment"),
});
});
}
getProductObj() {
if (this.props.product !== undefined) {
return this.props.product;
} else {
return this.state.product;
}
}
getCurrencySymbol(product) {
if (product?.currency === "USD") {
return "$";
} else if (product?.currency === "CNY") {
return "¥";
} else {
return "(Unknown currency)";
}
}
getCurrencyText(product) {
if (product?.currency === "USD") {
return i18next.t("product:USD");
} else if (product?.currency === "CNY") {
return i18next.t("product:CNY");
} else {
return "(Unknown currency)";
}
}
getProviders(product) {
if (this.state.providers.length === 0 || product.providers.length === 0) {
return [];
}
let providerMap = {};
this.state.providers.forEach(provider => {
providerMap[provider.name] = provider;
})
return product.providers.map(providerName => providerMap[providerName]);
}
getPayUrl(product, provider) {
if (product === null || provider === null) {
return "";
}
return `https://${provider.type}`;
// if (provider.type === "WeChat") {
// return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
// } else if (provider.type === "GitHub") {
// return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
// }
}
getPayButton(provider) {
let text = provider.type;
if (provider.type === "Alipay") {
text = i18next.t("product:Alipay");
} else if (provider.type === "WeChat Pay") {
text = i18next.t("product:WeChat Pay");
} else if (provider.type === "Paypal") {
text = i18next.t("product:Paypal");
}
return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
<img style={{marginRight: "10px"}} width={36} height={36} src={Provider.getProviderLogo(provider)} alt={provider.displayName} />
} size={"large"} >
{
text
}
</Button>
)
}
renderProviderButton(provider, product) {
return (
<span key={provider.name} style={{width: "200px", marginRight: "20px", marginBottom: "10px"}}>
<a style={{width: "200px"}} href={this.getPayUrl(product, provider)}>
{
this.getPayButton(provider)
}
</a>
</span>
)
}
renderPay(product) {
if (product === undefined || product === null) {
return null;
}
if (product.state !== "Published") {
return i18next.t("product:This product is currently not in sale.");
}
if (product.providers.length === 0) {
return i18next.t("product:There is no payment channel for this product.");
}
const providers = this.getProviders(product);
return providers.map(provider => {
return this.renderProviderButton(provider, product);
})
}
render() {
const product = this.getProductObj();
return (
<div>
<Descriptions title={i18next.t("product:Buy Product")} bordered>
<Descriptions.Item label={i18next.t("general:Name")} span={3}>
<span style={{fontSize: 28}}>
{product?.displayName}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Detail")}><span style={{fontSize: 16}}>{product?.detail}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}>
<img src={product?.image} alt={product?.image} height={90} style={{marginBottom: '20px'}}/>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>
{`${this.getCurrencySymbol(product)}${product?.price} (${this.getCurrencyText(product)})`}
</span>
</Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Quantity")}><span style={{fontSize: 16}}>{product?.quantity}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Sold")}><span style={{fontSize: 16}}>{product?.sold}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Pay")} span={3}>
{
this.renderPay(product)
}
</Descriptions.Item>
</Descriptions>
</div>
)
}
}
export default ProductBuyPage;

311
web/src/ProductEditPage.js Normal file
View File

@ -0,0 +1,311 @@
// 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 {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd';
import * as ProductBackend from "./backend/ProductBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
import * as ProviderBackend from "./backend/ProviderBackend";
import ProductBuyPage from "./ProductBuyPage";
const { Option } = Select;
class ProductEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
productName: props.match.params.productName,
product: null,
providers: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getProduct();
this.getPaymentProviders();
}
getProduct() {
ProductBackend.getProduct("admin", this.state.productName)
.then((product) => {
this.setState({
product: product,
});
});
}
getPaymentProviders() {
ProviderBackend.getProviders("admin")
.then((res) => {
this.setState({
providers: res.filter(provider => provider.category === "Payment"),
});
});
}
parseProductField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateProductField(key, value) {
value = this.parseProductField(key, value);
let product = this.state.product;
product[key] = value;
this.setState({
product: product,
});
}
renderProduct() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("product:New Product") : i18next.t("product:Edit Product")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.name} onChange={e => {
this.updateProductField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.displayName} onChange={e => {
this.updateProductField('displayName', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
</Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col>
<Col span={23} >
<Input prefix={<LinkOutlined/>} value={this.state.product.image} onChange={e => {
this.updateProductField('image', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}:
</Col>
<Col span={23} >
<a target="_blank" rel="noreferrer" href={this.state.product.image}>
<img src={this.state.product.image} alt={this.state.product.image} height={90} style={{marginBottom: '20px'}}/>
</a>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.tag} onChange={e => {
this.updateProductField('tag', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Detail"), i18next.t("product:Detail - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.product.detail} onChange={e => {
this.updateProductField('detail', 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"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.currency} onChange={(value => {
this.updateProductField('currency', value);
})}>
{
[
{id: 'USD', name: 'USD'},
{id: 'CNY', name: 'CNY'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.price} onChange={value => {
this.updateProductField('price', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.quantity} onChange={value => {
this.updateProductField('quantity', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber value={this.state.product.sold} onChange={value => {
this.updateProductField('sold', value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}} value={this.state.product.providers} onChange={(value => {this.updateProductField('providers', value);})}>
{
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.product.state} onChange={(value => {
this.updateProductField('state', value);
})}>
{
[
{id: 'Published', name: 'Published'},
{id: 'Draft', name: 'Draft'},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
{
this.renderPreview()
}
</Row>
</Card>
)
}
renderPreview() {
let buyUrl = `/products/${this.state.product.name}/buy`;
return (
<Col span={22} style={{display: "flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={buyUrl}>
<Button type="primary">{i18next.t("product:Test buy page..")}</Button>
</a>
<br/>
<br/>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<ProductBuyPage product={this.state.product} />
</div>
</Col>
)
}
submitProductEdit(willExist) {
let product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
productName: this.state.product.name,
});
if (willExist) {
this.props.history.push(`/products`);
} else {
this.props.history.push(`/products/${this.state.product.name}`);
}
} else {
Setting.showMessage("error", res.msg);
this.updateProductField('name', this.state.productName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
deleteProduct() {
ProductBackend.deleteProduct(this.state.product)
.then(() => {
this.props.history.push(`/products`);
})
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
});
}
render() {
return (
<div>
{
this.state.product !== null ? this.renderProduct() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitProductEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitProductEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default ProductEditPage;

295
web/src/ProductListPage.js Normal file
View File

@ -0,0 +1,295 @@
// 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 {Link} from "react-router-dom";
import {Button, Col, List, Popconfirm, Row, Table, Tooltip} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as ProductBackend from "./backend/ProductBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import {EditOutlined} from "@ant-design/icons";
class ProductListPage extends BaseListPage {
newProduct() {
const randomName = Setting.getRandomName();
return {
owner: "admin",
name: `product_${randomName}`,
createdTime: moment().format(),
displayName: `New Product - ${randomName}`,
image: "https://cdn.casdoor.com/logo/casdoor-logo_1185x256.png",
tag: "Casdoor Summit 2022",
currency: "USD",
price: 300,
quantity: 99,
sold: 10,
providers: [],
state: "Published",
}
}
addProduct() {
const newProduct = this.newProduct();
ProductBackend.addProduct(newProduct)
.then((res) => {
this.props.history.push({pathname: `/products/${newProduct.name}`, mode: "add"});
}
)
.catch(error => {
Setting.showMessage("error", `Product failed to add: ${error}`);
});
}
deleteProduct(i) {
ProductBackend.deleteProduct(this.state.data[i])
.then((res) => {
Setting.showMessage("success", `Product deleted successfully`);
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
}
)
.catch(error => {
Setting.showMessage("error", `Product failed to delete: ${error}`);
});
}
renderTable(products) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '140px',
fixed: 'left',
sorter: true,
...this.getColumnSearchProps('name'),
render: (text, record, index) => {
return (
<Link to={`/products/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '160px',
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("general:Display name"),
dataIndex: 'displayName',
key: 'displayName',
width: '170px',
sorter: true,
...this.getColumnSearchProps('displayName'),
},
{
title: i18next.t("product:Image"),
dataIndex: 'image',
key: 'image',
width: '170px',
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
<img src={text} alt={text} width={150} />
</a>
)
}
},
{
title: i18next.t("product:Tag"),
dataIndex: 'tag',
key: 'tag',
width: '160px',
sorter: true,
...this.getColumnSearchProps('tag'),
},
{
title: i18next.t("product:Currency"),
dataIndex: 'currency',
key: 'currency',
width: '120px',
sorter: true,
...this.getColumnSearchProps('currency'),
},
{
title: i18next.t("product:Price"),
dataIndex: 'price',
key: 'price',
width: '120px',
sorter: true,
...this.getColumnSearchProps('price'),
},
{
title: i18next.t("product:Quantity"),
dataIndex: 'quantity',
key: 'quantity',
width: '120px',
sorter: true,
...this.getColumnSearchProps('quantity'),
},
{
title: i18next.t("product:Sold"),
dataIndex: 'sold',
key: 'sold',
width: '120px',
sorter: true,
...this.getColumnSearchProps('sold'),
},
{
title: i18next.t("general:State"),
dataIndex: 'state',
key: 'state',
width: '120px',
sorter: true,
...this.getColumnSearchProps('state'),
},
{
title: i18next.t("product:Payment providers"),
dataIndex: 'providers',
key: 'providers',
width: '500px',
...this.getColumnSearchProps('providers'),
render: (text, record, index) => {
const providers = text;
if (providers.length === 0) {
return "(empty)";
}
const half = Math.floor((providers.length + 1) / 2);
const getList = (providers) => {
return (
<List
size="small"
locale={{emptyText: " "}}
dataSource={providers}
renderItem={(providerName, i) => {
return (
<List.Item>
<div style={{display: "inline"}}>
<Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/providers/${providerName}`)} />
</Tooltip>
<Link to={`/providers/${providerName}`}>
{providerName}
</Link>
</div>
</List.Item>
)
}}
/>
)
}
return (
<div>
<Row>
<Col span={12}>
{
getList(providers.slice(0, half))
}
</Col>
<Col span={12}>
{
getList(providers.slice(half))
}
</Col>
</Row>
</div>
)
},
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/products/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete product: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={products} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Products")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addProduct.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
</div>
);
}
fetch = (params = {}) => {
let field = params.searchedColumn, value = params.searchText;
let sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({ loading: true });
ProductBackend.getProducts("", params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
if (res.status === "ok") {
this.setState({
loading: false,
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
}
});
};
}
export default ProductListPage;

View File

@ -31,6 +31,7 @@ class ProviderEditPage extends React.Component {
classes: props,
providerName: props.match.params.providerName,
provider: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -133,9 +134,10 @@ class ProviderEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("provider:Edit Provider")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("provider:New Provider") : i18next.t("provider:Edit Provider")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitProviderEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitProviderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteProvider()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -572,6 +574,16 @@ class ProviderEditPage extends React.Component {
});
}
deleteProvider() {
ProviderBackend.deleteProvider(this.state.provider)
.then(() => {
this.props.history.push(`/providers`);
})
.catch(error => {
Setting.showMessage("error", `Provider failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -581,6 +593,7 @@ class ProviderEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitProviderEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitProviderEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteProvider()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -47,8 +47,7 @@ class ProviderListPage extends BaseListPage {
const newProvider = this.newProvider();
ProviderBackend.addProvider(newProvider)
.then((res) => {
Setting.showMessage("success", `Provider added successfully`);
this.props.history.push(`/providers/${newProvider.name}`);
this.props.history.push({pathname: `/providers/${newProvider.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -33,6 +33,7 @@ class RoleEditPage extends React.Component {
organizations: [],
users: [],
roles: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -101,9 +102,10 @@ class RoleEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("role:Edit Role")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("role:New Role") : i18next.t("role:Edit Role")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitRoleEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitRoleEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteRole()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -201,6 +203,16 @@ class RoleEditPage extends React.Component {
});
}
deleteRole() {
RoleBackend.deleteRole(this.state.role)
.then(() => {
this.props.history.push(`/roles`);
})
.catch(error => {
Setting.showMessage("error", `Role failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -210,6 +222,7 @@ class RoleEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitRoleEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitRoleEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteRole()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -39,8 +39,7 @@ class RoleListPage extends BaseListPage {
const newRole = this.newRole();
RoleBackend.addRole(newRole)
.then((res) => {
Setting.showMessage("success", `Role added successfully`);
this.props.history.push(`/roles/${newRole.owner}/${newRole.name}`);
this.props.history.push({pathname: `/roles/${newRole.owner}/${newRole.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -325,6 +325,10 @@ export function getAvatarColor(s) {
return colorList[random % 4];
}
export function getLanguage() {
return i18next.language;
}
export function setLanguage(language) {
localStorage.setItem("language", language);
changeMomentLanguage(language);

View File

@ -170,7 +170,8 @@ class SignupTable extends React.Component {
} if (record.name === "Display name") {
options = [
{id: 'None', name: 'None'},
{id: 'Personal', name: 'Personal'},
{id: 'Real name', name: 'Real name'},
{id: 'First, last', name: 'First, last'},
];
}

View File

@ -36,6 +36,7 @@ class SyncerEditPage extends React.Component {
syncerName: props.match.params.syncerName,
syncer: null,
organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -83,9 +84,10 @@ class SyncerEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("syncer:Edit Syncer")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("syncer:New Syncer") : i18next.t("syncer:Edit Syncer")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitSyncerEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitSyncerEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteSyncer()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -308,6 +310,16 @@ class SyncerEditPage extends React.Component {
});
}
deleteSyncer() {
SyncerBackend.deleteSyncer(this.state.syncer)
.then(() => {
this.props.history.push(`/syncers`);
})
.catch(error => {
Setting.showMessage("error", `Syncer failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -317,6 +329,7 @@ class SyncerEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitSyncerEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitSyncerEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteSyncer()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -50,8 +50,7 @@ class SyncerListPage extends BaseListPage {
const newSyncer = this.newSyncer();
SyncerBackend.addSyncer(newSyncer)
.then((res) => {
Setting.showMessage("success", `Syncer added successfully`);
this.props.history.push(`/syncers/${newSyncer.name}`);
this.props.history.push({pathname: `/syncers/${newSyncer.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -25,6 +25,7 @@ class TokenEditPage extends React.Component {
classes: props,
tokenName: props.match.params.tokenName,
token: null,
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -62,9 +63,10 @@ class TokenEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("token:Edit Token")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("token:New Token") : i18next.t("token:Edit Token")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitTokenEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitTokenEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteToken()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -186,6 +188,16 @@ class TokenEditPage extends React.Component {
});
}
deleteToken() {
TokenBackend.deleteToken(this.state.token)
.then(() => {
this.props.history.push(`/tokens`);
})
.catch(error => {
Setting.showMessage("error", `Token failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -195,6 +207,7 @@ class TokenEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitTokenEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitTokenEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteToken()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -44,8 +44,7 @@ class TokenListPage extends BaseListPage {
const newToken = this.newToken();
TokenBackend.addToken(newToken)
.then((res) => {
Setting.showMessage("success", `Token added successfully`);
this.props.history.push(`/tokens/${newToken.name}`);
this.props.history.push({pathname: `/tokens/${newToken.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -46,6 +46,7 @@ class UserEditPage extends React.Component {
application: null,
organizations: [],
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -121,9 +122,10 @@ class UserEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -430,6 +432,16 @@ class UserEditPage extends React.Component {
});
}
deleteUser() {
UserBackend.deleteUser(this.state.user)
.then(() => {
this.props.history.push(`/users`);
})
.catch(error => {
Setting.showMessage("error", `User failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -439,6 +451,7 @@ class UserEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -21,6 +21,7 @@ import * as Setting from "./Setting";
import * as UserBackend from "./backend/UserBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import * as path from "path";
class UserListPage extends BaseListPage {
constructor(props) {
@ -69,8 +70,7 @@ class UserListPage extends BaseListPage {
const newUser = this.newUser();
UserBackend.addUser(newUser)
.then((res) => {
Setting.showMessage("success", `User added successfully`);
this.props.history.push(`/users/${newUser.owner}/${newUser.name}`);
this.props.history.push({pathname: `/users/${newUser.owner}/${newUser.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -96,6 +96,7 @@ class WebhookEditPage extends React.Component {
webhookName: props.match.params.webhookName,
webhook: null,
organizations: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
@ -149,9 +150,10 @@ class WebhookEditPage extends React.Component {
return (
<Card size="small" title={
<div>
{i18next.t("webhook:Edit Webhook")}&nbsp;&nbsp;&nbsp;&nbsp;
{this.state.mode === "add" ? i18next.t("webhook:New Webhook") : i18next.t("webhook:Edit Webhook")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitWebhookEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitWebhookEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteWebhook()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
@ -315,6 +317,16 @@ class WebhookEditPage extends React.Component {
});
}
deleteWebhook() {
WebhookBackend.deleteWebhook(this.state.webhook)
.then(() => {
this.props.history.push(`/webhooks`);
})
.catch(error => {
Setting.showMessage("error", `Webhook failed to delete: ${error}`);
});
}
render() {
return (
<div>
@ -324,6 +336,7 @@ class WebhookEditPage extends React.Component {
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitWebhookEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitWebhookEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteWebhook()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);

View File

@ -42,8 +42,7 @@ class WebhookListPage extends BaseListPage {
const newWebhook = this.newWebhook();
WebhookBackend.addWebhook(newWebhook)
.then((res) => {
Setting.showMessage("success", `Webhook added successfully`);
this.props.history.push(`/webhooks/${newWebhook.name}`);
this.props.history.push({pathname: `/webhooks/${newWebhook.name}`, mode: "add"});
}
)
.catch(error => {

View File

@ -18,7 +18,7 @@ import UserEditPage from "../UserEditPage";
class AccountPage extends React.Component {
render() {
return (
<UserEditPage organizationName={this.props.account.owner} userName={this.props.account.name} account={this.props.account} />
<UserEditPage organizationName={this.props.account.owner} userName={this.props.account.name} account={this.props.account} location={this.props.location} />
)
}
}

View File

@ -58,6 +58,10 @@ class AuthCallback extends React.Component {
if (authServerUrl === realRedirectUrl) {
return "login";
} else {
const responseType = innerParams.get("response_type");
if (responseType !== null) {
return responseType
}
return "code";
}
} else if (method === "link") {
@ -116,6 +120,9 @@ class AuthCallback extends React.Component {
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}?code=${code}&state=${oAuthParams.state}`);
// Util.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token"){
const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}?${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
} else if (responseType === "link") {
const from = innerParams.get("from");
Setting.goToLinkSoft(this, from);

View File

@ -116,14 +116,18 @@ class LoginPage extends React.Component {
onFinish(values) {
const application = this.getApplicationObj();
const ths = this;
values["type"] = this.state.type;
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
const oAuthParams = Util.getOAuthGetParameters();
if (oAuthParams !== null && oAuthParams.responseType!= null && oAuthParams.responseType !== "") {
values["type"] = oAuthParams.responseType
}else{
values["type"] = this.state.type;
}
values["phonePrefix"] = this.getApplicationObj()?.organizationObj.phonePrefix;
AuthBackend.login(values, oAuthParams)
.then((res) => {
if (res.status === 'ok') {
const responseType = this.state.type;
const responseType = values["type"];
if (responseType === "login") {
Util.showMessage("success", `Logged in successfully`);
@ -156,6 +160,9 @@ class LoginPage extends React.Component {
}
// Util.showMessage("success", `Authorization code: ${res.data}`);
} else if (responseType === "token" || responseType === "id_token") {
const accessToken = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}#${responseType}=${accessToken}?state=${oAuthParams.state}&token_type=bearer`);
}
} else {
Util.showMessage("error", `Failed to log in: ${res.msg}`);

View File

@ -192,15 +192,50 @@ class SignupPage extends React.Component {
</Form.Item>
)
} else if (signupItem.name === "Display name") {
if (signupItem.rule === "First, last" && Setting.getLanguage() !== "zh") {
return (
<React.Fragment>
<Form.Item
name="firstName"
key="firstName"
label={i18next.t("general:First name")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your first name!"),
whitespace: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="lastName"
key="lastName"
label={i18next.t("general:Last name")}
rules={[
{
required: required,
message: i18next.t("signup:Please input your last name!"),
whitespace: true,
},
]}
>
<Input />
</Form.Item>
</React.Fragment>
)
}
return (
<Form.Item
name="name"
key="name"
label={signupItem.rule === "Personal" ? i18next.t("general:Personal name") : i18next.t("general:Display name")}
label={(signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("general:Real name") : i18next.t("general:Display name")}
rules={[
{
required: required,
message: signupItem.rule === "Personal" ? i18next.t("signup:Please input your personal name!") : i18next.t("signup:Please input your display name!"),
message: (signupItem.rule === "Real name" || signupItem.rule === "First, last") ? i18next.t("signup:Please input your real name!") : i18next.t("signup:Please input your display name!"),
whitespace: true,
},
]}

View File

@ -0,0 +1,56 @@
// 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 * as Setting from "../Setting";
export function getProducts(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-products?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getProduct(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-product?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateProduct(owner, name, product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/update-product?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}
export function addProduct(product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/add-product`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}
export function deleteProduct(product) {
let newProduct = Setting.deepCopy(product);
return fetch(`${Setting.ServerUrl}/api/delete-product`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newProduct),
}).then(res => res.json());
}

View File

@ -14,6 +14,9 @@
"Enable signup": "Anmeldung aktivieren",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Datei erfolgreich hochgeladen",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Passwort AN",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Bitte wählen Sie eine HTML-Datei",
@ -45,6 +48,7 @@
"Edit Cert": "Zitat bearbeiten",
"Expire in years": "Gültig in Jahren",
"Expire in years - Tooltip": "Verfällt in Jahren - Tooltip",
"New Cert": "New Cert",
"Private key": "Privater Schlüssel",
"Private key - Tooltip": "Privater Schlüssel - Tooltip",
"Private key copied to clipboard successfully": "Privater Schlüssel erfolgreich in die Zwischenablage kopiert",
@ -91,6 +95,7 @@
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Zurück zu Hause",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@ -109,6 +114,7 @@
"Email": "E-Mail",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URL vergessen",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Zuhause",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Ist aktiviert - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Master-Passwort",
"Master password - Tooltip": "Masterpasswort - Tooltip",
@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Berechtigungen",
"Personal name": "Persönlicher Name",
"Phone": "Telefon",
"Phone - Tooltip": "Phone",
"Phone prefix": "Telefonpräfix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Vorschau",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Anbieter",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Anbieter",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Persönlicher Name",
"Records": "Datensätze",
"Request URI": "Request URI",
"Resources": "Ressourcen",
@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Die von Ihnen besuchte Seite existiert leider nicht.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Zeitstempel",
"Tokens": "Token",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Hoch",
"User": "Benutzer",
"User - Tooltip": "Benutzer - Tooltip",
@ -233,6 +244,7 @@
"Default avatar": "Standard Avatar",
"Edit Organization": "Organisation bearbeiten",
"Favicon": "Févicon",
"New Organization": "New Organization",
"Soft deletion": "Weiche Löschung",
"Soft deletion - Tooltip": "Weiche Löschung - Tooltip",
"Website URL": "Website-URL",
@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Aktionen",
@ -253,10 +266,42 @@
"Edit Permission": "Berechtigung bearbeiten",
"Effect": "Effekt",
"Effect - Tooltip": "Effekt - Tooltip",
"New Permission": "New Permission",
"Resource type": "Ressourcentyp",
"Resource type - Tooltip": "Ressourcentyp - Tooltip",
"Resources": "Ressourcen"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Zugangsschlüssel",
"Access key - Tooltip": "Zugriffsschlüssel - Tooltip",
@ -299,6 +344,7 @@
"Method": "Methode",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Name",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Metadaten erfolgreich analysieren",
"Port": "Port",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Rolle bearbeiten",
"New Role": "New Role",
"Sub roles": "Unterrollen",
"Sub roles - Tooltip": "Unterrollen - Tooltip",
"Sub users": "Unternutzer",
@ -381,8 +428,10 @@
"Please input your address!": "Bitte geben Sie Ihre Adresse ein!",
"Please input your affiliation!": "Bitte geben Sie Ihre Zugehörigkeit ein!",
"Please input your display name!": "Bitte geben Sie Ihren Anzeigenamen ein!",
"Please input your personal name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Bitte geben Sie Ihre Telefonnummer ein!",
"Please input your real name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region!",
"Terms of Use": "Nutzungsbedingungen",
"The input is not valid Email!": "Die Eingabe ist ungültig!",
@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Ist gehasht",
"New Syncer": "New Syncer",
"Sync interval": "Sync-Intervall",
"Sync interval - Tooltip": "Sync-Intervall - Tooltip",
"Table": "Tisch",
@ -424,6 +474,7 @@
"Authorization code": "Autorisierungscode",
"Edit Token": "Token bearbeiten",
"Expires in": "Läuft ab",
"New Token": "New Token",
"Scope": "Bereich",
"Token type": "Token-Typ"
},
@ -462,6 +513,7 @@
"Modify password...": "Passwort ändern...",
"New Email": "Neue E-Mail",
"New Password": "Neues Passwort",
"New User": "New User",
"New phone": "Neues Telefon",
"OK": "Ok",
"Old Password": "Altes Passwort",
@ -498,6 +550,7 @@
"Method": "Methode",
"Method - Tooltip": "Methode - Tooltip",
"Name": "Name",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Wert"

View File

@ -14,6 +14,9 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Enable signup - Tooltip",
"File uploaded successfully": "File uploaded successfully",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Password ON",
"Password ON - Tooltip": "Password ON - Tooltip",
"Please select a HTML file": "Please select a HTML file",
@ -45,6 +48,7 @@
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
@ -91,6 +95,7 @@
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar - Tooltip",
"Back Home": "Back Home",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@ -109,6 +114,7 @@
"Email": "Email",
"Email - Tooltip": "Email - Tooltip",
"Favicon - Tooltip": "Favicon - Tooltip",
"First name": "First name",
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Forget URL - Tooltip",
"Home": "Home",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Is enabled - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "Logo - Tooltip",
"Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip",
@ -137,17 +144,18 @@
"Password type - Tooltip": "Password type - Tooltip",
"Payments": "Payments",
"Permissions": "Permissions",
"Personal name": "Personal name",
"Phone": "Phone",
"Phone - Tooltip": "Phone - Tooltip",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Phone prefix - Tooltip",
"Preview": "Preview",
"Preview - Tooltip": "Preview - Tooltip",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Providers",
"Providers - Tooltip": "Providers - Tooltip",
"Real name": "Real name",
"Records": "Records",
"Request URI": "Request URI",
"Resources": "Resources",
@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Up",
"User": "User",
"User - Tooltip": "User - Tooltip",
@ -233,6 +244,7 @@
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
"New Organization": "New Organization",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Website URL": "Website URL",
@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Actions",
@ -253,10 +266,42 @@
"Edit Permission": "Edit Permission",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "New Permission",
"Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
@ -299,6 +344,7 @@
"Method": "Method",
"Method - Tooltip": "Method - Tooltip",
"Name": "Name",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully",
"Port": "Port",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Edit Role",
"New Role": "New Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
@ -381,8 +428,10 @@
"Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!",
"Please input your personal name!": "Please input your personal name!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not valid Email!": "The input is not valid Email!",
@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Is hashed",
"New Syncer": "New Syncer",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Sync interval - Tooltip",
"Table": "Table",
@ -424,6 +474,7 @@
"Authorization code": "Authorization code",
"Edit Token": "Edit Token",
"Expires in": "Expires in",
"New Token": "New Token",
"Scope": "Scope",
"Token type": "Token type"
},
@ -462,6 +513,7 @@
"Modify password...": "Modify password...",
"New Email": "New Email",
"New Password": "New Password",
"New User": "New User",
"New phone": "New phone",
"OK": "OK",
"Old Password": "Old Password",
@ -498,6 +550,7 @@
"Method": "Method",
"Method - Tooltip": "Method - Tooltip",
"Name": "Name",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Value"

View File

@ -14,6 +14,9 @@
"Enable signup": "Activer l'inscription",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Fichier téléchargé avec succès",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Mot de passe activé",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Veuillez sélectionner un fichier HTML",
@ -45,6 +48,7 @@
"Edit Cert": "Modifier le certificat",
"Expire in years": "Expire dans les années",
"Expire in years - Tooltip": "Expire dans les années - infobulle",
"New Cert": "New Cert",
"Private key": "Clé privée",
"Private key - Tooltip": "Clé privée - Infobulle",
"Private key copied to clipboard successfully": "Clé privée copiée dans le presse-papiers avec succès",
@ -91,6 +95,7 @@
"Avatar": "Avatars",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Retour à la page d'accueil",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@ -109,6 +114,7 @@
"Email": "Courriel",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Oublier l'URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Domicile",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Est activé - infobulle",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Infobulle",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Mot de passe maître",
"Master password - Tooltip": "Mot de passe maître - Infobulle",
@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Permissions",
"Personal name": "Nom personnel",
"Phone": "Téléphone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Préfixe du téléphone",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Aperçu",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Fournisseur",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Fournisseurs",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Nom personnel",
"Records": "Enregistrements",
"Request URI": "Request URI",
"Resources": "Ressource",
@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Synchronisateurs",
"Timestamp": "Horodatage",
"Tokens": "Jetons",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Monter",
"User": "Utilisateur",
"User - Tooltip": "Utilisateur - infobulle",
@ -233,6 +244,7 @@
"Default avatar": "Avatar par défaut",
"Edit Organization": "Modifier l'organisation",
"Favicon": "Favicon",
"New Organization": "New Organization",
"Soft deletion": "Suppression du logiciel",
"Soft deletion - Tooltip": "Suppression de soft - infobulle",
"Website URL": "URL du site web",
@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Actions",
@ -253,10 +266,42 @@
"Edit Permission": "Autorisation d'édition",
"Effect": "Effet",
"Effect - Tooltip": "Effet - Infobulle",
"New Permission": "New Permission",
"Resource type": "Type de ressource",
"Resource type - Tooltip": "Type de ressource - infobulle",
"Resources": "Ressource"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Clé d'accès",
"Access key - Tooltip": "Touche d'accès - Infobulle",
@ -299,6 +344,7 @@
"Method": "Méthode",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Nom",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Analyse des métadonnées réussie",
"Port": "Port",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Modifier le rôle",
"New Role": "New Role",
"Sub roles": "Sous-rôles",
"Sub roles - Tooltip": "Sous-rôles - infobulle",
"Sub users": "Sous-utilisateurs",
@ -381,8 +428,10 @@
"Please input your address!": "Veuillez saisir votre adresse !",
"Please input your affiliation!": "Veuillez entrer votre affiliation !",
"Please input your display name!": "Veuillez entrer votre nom d'affichage !",
"Please input your personal name!": "Veuillez entrer votre nom personnel !",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Veuillez entrer votre numéro de téléphone!",
"Please input your real name!": "Veuillez entrer votre nom personnel !",
"Please select your country/region!": "Veuillez sélectionner votre pays/région!",
"Terms of Use": "Conditions d'utilisation",
"The input is not valid Email!": "L'entrée n'est pas un email valide !",
@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Est haché",
"New Syncer": "New Syncer",
"Sync interval": "Intervalle de synchronisation",
"Sync interval - Tooltip": "Intervalle de synchronisation - infobulle",
"Table": "Tableau",
@ -424,6 +474,7 @@
"Authorization code": "Code d'autorisation",
"Edit Token": "Modifier le jeton",
"Expires in": "Expire dans",
"New Token": "New Token",
"Scope": "Périmètre d'application",
"Token type": "Type de jeton"
},
@ -462,6 +513,7 @@
"Modify password...": "Modifier le mot de passe...",
"New Email": "Nouvel e-mail",
"New Password": "Nouveau mot de passe",
"New User": "New User",
"New phone": "Nouveau téléphone",
"OK": "Ok",
"Old Password": "Ancien mot de passe",
@ -498,6 +550,7 @@
"Method": "Méthode",
"Method - Tooltip": "Méthode - Infobulle",
"Name": "Nom",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Info-bulle",
"Value": "Valeur"

View File

@ -14,6 +14,9 @@
"Enable signup": "サインアップを有効にする",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "ファイルが正常にアップロードされました",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "パスワードON",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "HTMLファイルを選択してください",
@ -45,6 +48,7 @@
"Edit Cert": "Certを編集",
"Expire in years": "有効期限",
"Expire in years - Tooltip": "年間有効期限 - ツールチップ",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "秘密鍵を正常にクリップボードにコピーしました",
@ -91,6 +95,7 @@
"Avatar": "アバター",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "ホーム",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@ -109,6 +114,7 @@
"Email": "Eメールアドレス",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "URLを忘れた",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "ホーム",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "有効にする - ツールチップ",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAP - ツールチップ",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "マスターパスワード",
"Master password - Tooltip": "マスターパスワード - ツールチップ",
@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "アクセス許可",
"Personal name": "個人名",
"Phone": "電話番号",
"Phone - Tooltip": "Phone",
"Phone prefix": "電話プレフィクス",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "プレビュー",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "プロバイダー",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "プロバイダー",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "個人名",
"Records": "レコード",
"Request URI": "Request URI",
"Resources": "リソース",
@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "申し訳ありませんが、訪問したページは存在しません。",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "タイムスタンプ",
"Tokens": "トークン",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "上へ",
"User": "ユーザー",
"User - Tooltip": "ユーザー → ツールチップ",
@ -233,6 +244,7 @@
"Default avatar": "デフォルトのアバター",
"Edit Organization": "組織を編集",
"Favicon": "ファビコン",
"New Organization": "New Organization",
"Soft deletion": "ソフト削除",
"Soft deletion - Tooltip": "ソフト削除 - ツールチップ",
"Website URL": "Website URL",
@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "アクション",
@ -253,10 +266,42 @@
"Edit Permission": "権限を編集",
"Effect": "効果",
"Effect - Tooltip": "エフェクト - ツールチップ",
"New Permission": "New Permission",
"Resource type": "リソースタイプ",
"Resource type - Tooltip": "リソースタイプ - ツールチップ",
"Resources": "リソース"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "アクセスキー",
"Access key - Tooltip": "アクセスキー → ツールチップ",
@ -299,6 +344,7 @@
"Method": "方法",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "名前",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "メタデータの解析に成功",
"Port": "ポート",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "役割を編集",
"New Role": "New Role",
"Sub roles": "サブロール",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "サブユーザー",
@ -381,8 +428,10 @@
"Please input your address!": "住所を入力してください!",
"Please input your affiliation!": "所属を入力してください!",
"Please input your display name!": "表示名を入力してください。",
"Please input your personal name!": "個人名を入力してください!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "電話番号を入力してください!",
"Please input your real name!": "個人名を入力してください!",
"Please select your country/region!": "あなたの国/地域を選択してください!",
"Terms of Use": "利用規約",
"The input is not valid Email!": "入力されたメールアドレスが無効です!",
@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "ハッシュされました",
"New Syncer": "New Syncer",
"Sync interval": "同期間隔",
"Sync interval - Tooltip": "同期間隔 - ツールチップ",
"Table": "表",
@ -424,6 +474,7 @@
"Authorization code": "認証コード",
"Edit Token": "トークンを編集",
"Expires in": "有効期限:",
"New Token": "New Token",
"Scope": "スコープ",
"Token type": "トークンの種類"
},
@ -462,6 +513,7 @@
"Modify password...": "パスワードを変更...",
"New Email": "新しいメール",
"New Password": "新しいパスワード",
"New User": "New User",
"New phone": "新しい電話番号",
"OK": "OK",
"Old Password": "古いパスワード",
@ -498,6 +550,7 @@
"Method": "方法",
"Method - Tooltip": "方法 - ツールチップ",
"Name": "名前",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL → ツールチップ",
"Value": "値"

View File

@ -14,6 +14,9 @@
"Enable signup": "Enable signup",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "File uploaded successfully",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Password ON",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Please select a HTML file",
@ -45,6 +48,7 @@
"Edit Cert": "Edit Cert",
"Expire in years": "Expire in years",
"Expire in years - Tooltip": "Expire in years - Tooltip",
"New Cert": "New Cert",
"Private key": "Private key",
"Private key - Tooltip": "Private key - Tooltip",
"Private key copied to clipboard successfully": "Private key copied to clipboard successfully",
@ -91,6 +95,7 @@
"Avatar": "Avatar",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Back Home",
"Cancel": "Cancel",
"Captcha": "Captcha",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@ -109,6 +114,7 @@
"Email": "Email",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Forget URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Home",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Is enabled - Tooltip",
"LDAPs": "LDAPs",
"LDAPs - Tooltip": "LDAPs - Tooltip",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Master password",
"Master password - Tooltip": "Master password - Tooltip",
@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Permissions",
"Personal name": "Personal name",
"Phone": "Phone",
"Phone - Tooltip": "Phone",
"Phone prefix": "Phone prefix",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Preview",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Provider",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Providers",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Real name",
"Records": "Records",
"Request URI": "Request URI",
"Resources": "Resources",
@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Syncers",
"Timestamp": "Timestamp",
"Tokens": "Tokens",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Up",
"User": "User",
"User - Tooltip": "User - Tooltip",
@ -233,6 +244,7 @@
"Default avatar": "Default avatar",
"Edit Organization": "Edit Organization",
"Favicon": "Favicon",
"New Organization": "New Organization",
"Soft deletion": "Soft deletion",
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
"Website URL": "Website URL",
@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Actions",
@ -253,10 +266,42 @@
"Edit Permission": "Edit Permission",
"Effect": "Effect",
"Effect - Tooltip": "Effect - Tooltip",
"New Permission": "New Permission",
"Resource type": "Resource type",
"Resource type - Tooltip": "Resource type - Tooltip",
"Resources": "Resources"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Access key",
"Access key - Tooltip": "Access key - Tooltip",
@ -299,6 +344,7 @@
"Method": "Method",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Name",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Parse Metadata successfully",
"Port": "Port",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Edit Role",
"New Role": "New Role",
"Sub roles": "Sub roles",
"Sub roles - Tooltip": "Sub roles - Tooltip",
"Sub users": "Sub users",
@ -381,8 +428,10 @@
"Please input your address!": "Please input your address!",
"Please input your affiliation!": "Please input your affiliation!",
"Please input your display name!": "Please input your display name!",
"Please input your personal name!": "Please input your personal name!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Please input your phone number!",
"Please input your real name!": "Please input your real name!",
"Please select your country/region!": "Please select your country/region!",
"Terms of Use": "Terms of Use",
"The input is not valid Email!": "The input is not valid Email!",
@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Is hashed",
"New Syncer": "New Syncer",
"Sync interval": "Sync interval",
"Sync interval - Tooltip": "Sync interval - Tooltip",
"Table": "Table",
@ -424,6 +474,7 @@
"Authorization code": "Authorization code",
"Edit Token": "Edit Token",
"Expires in": "Expires in",
"New Token": "New Token",
"Scope": "Scope",
"Token type": "Token type"
},
@ -462,6 +513,7 @@
"Modify password...": "Modify password...",
"New Email": "New Email",
"New Password": "New Password",
"New User": "New User",
"New phone": "New phone",
"OK": "OK",
"Old Password": "Old Password",
@ -498,6 +550,7 @@
"Method": "Method",
"Method - Tooltip": "Method - Tooltip",
"Name": "Name",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Value": "Value"

View File

@ -14,6 +14,9 @@
"Enable signup": "Включить регистрацию",
"Enable signup - Tooltip": "Whether to allow users to sign up",
"File uploaded successfully": "Файл успешно загружен",
"Grant types": "Grant types",
"Grant types - Tooltip": "Grant types - Tooltip",
"New Application": "New Application",
"Password ON": "Пароль ВКЛ",
"Password ON - Tooltip": "Whether to allow password login",
"Please select a HTML file": "Пожалуйста, выберите HTML-файл",
@ -45,6 +48,7 @@
"Edit Cert": "Изменить сертификат",
"Expire in years": "Истекает через годы",
"Expire in years - Tooltip": "Истекает через годы - Подсказка",
"New Cert": "New Cert",
"Private key": "Приватный ключ",
"Private key - Tooltip": "Приватный ключ - Подсказка",
"Private key copied to clipboard successfully": "Приватный ключ скопирован в буфер обмена",
@ -91,6 +95,7 @@
"Avatar": "Аватар",
"Avatar - Tooltip": "Avatar to show to others",
"Back Home": "Назад",
"Cancel": "Cancel",
"Captcha": "Капча",
"Cert": "Cert",
"Cert - Tooltip": "Cert - Tooltip",
@ -109,6 +114,7 @@
"Email": "Почта",
"Email - Tooltip": "email",
"Favicon - Tooltip": "Application icon",
"First name": "First name",
"Forget URL": "Забыть URL",
"Forget URL - Tooltip": "Unique string-style identifier",
"Home": "Домашний",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "Включено - Подсказка",
"LDAPs": "LDAPы",
"LDAPs - Tooltip": "LDAPs - Подсказки",
"Last name": "Last name",
"Logo - Tooltip": "App's image tag",
"Master password": "Мастер-пароль",
"Master password - Tooltip": "Мастер-пароль - Tooltip",
@ -137,17 +144,18 @@
"Password type - Tooltip": "The form in which the password is stored in the database",
"Payments": "Payments",
"Permissions": "Права доступа",
"Personal name": "Личное имя",
"Phone": "Телефон",
"Phone - Tooltip": "Phone",
"Phone prefix": "Префикс телефона",
"Phone prefix - Tooltip": "Mobile phone number prefix, used to distinguish countries or regions",
"Preview": "Предпросмотр",
"Preview - Tooltip": "The form in which the password is stored in the database",
"Products": "Products",
"Provider": "Поставщик",
"Provider - Tooltip": "Provider - Tooltip",
"Providers": "Поставщики",
"Providers - Tooltip": "List of third-party applications that can be used to log in",
"Real name": "Личное имя",
"Records": "Отчеты",
"Request URI": "Request URI",
"Resources": "Ресурсы",
@ -161,11 +169,14 @@
"Signup application": "Signup application",
"Signup application - Tooltip": "Signup application - Tooltip",
"Sorry, the page you visited does not exist.": "Извините, посещенная вами страница не существует.",
"State": "State",
"State - Tooltip": "State - Tooltip",
"Swagger": "Swagger",
"Syncers": "Синхронизаторы",
"Timestamp": "Отметка времени",
"Tokens": "Жетоны",
"URL": "URL",
"URL - Tooltip": "URL - Tooltip",
"Up": "Вверх",
"User": "Пользователь",
"User - Tooltip": "Пользователь - Подсказка",
@ -233,6 +244,7 @@
"Default avatar": "Аватар по умолчанию",
"Edit Organization": "Изменить организацию",
"Favicon": "Иконка",
"New Organization": "New Organization",
"Soft deletion": "Мягкое удаление",
"Soft deletion - Tooltip": "Мягкое удаление - Подсказка",
"Website URL": "URL сайта",
@ -245,7 +257,8 @@
"Currency - Tooltip": "Currency - Tooltip",
"Edit Payment": "Edit Payment",
"Good": "Good",
"Good - Tooltip": "Good - Tooltip"
"Good - Tooltip": "Good - Tooltip",
"New Payment": "New Payment"
},
"permission": {
"Actions": "Действия",
@ -253,10 +266,42 @@
"Edit Permission": "Изменить права доступа",
"Effect": "Эффект",
"Effect - Tooltip": "Эффект - Подсказка",
"New Permission": "New Permission",
"Resource type": "Тип ресурса",
"Resource type - Tooltip": "Тип ресурса - Подсказка",
"Resources": "Ресурсы"
},
"product": {
"Alipay": "Alipay",
"Buy Product": "Buy Product",
"CNY": "CNY",
"Currency": "Currency",
"Currency - Tooltip": "Currency - Tooltip",
"Detail": "Detail",
"Detail - Tooltip": "Detail - Tooltip",
"Edit Product": "Edit Product",
"Image": "Image",
"Image - Tooltip": "Image - Tooltip",
"New Product": "New Product",
"Pay": "Pay",
"Payment providers": "Payment providers",
"Payment providers - Tooltip": "Payment providers - Tooltip",
"Paypal": "Paypal",
"Price": "Price",
"Price - Tooltip": "Price - Tooltip",
"Quantity": "Quantity",
"Quantity - Tooltip": "Quantity - Tooltip",
"SKU": "SKU",
"Sold": "Sold",
"Sold - Tooltip": "Sold - Tooltip",
"Tag": "Tag",
"Tag - Tooltip": "Tag - Tooltip",
"Test buy page..": "Test buy page..",
"There is no payment channel for this product.": "There is no payment channel for this product.",
"This product is currently not in sale.": "This product is currently not in sale.",
"USD": "USD",
"WeChat Pay": "WeChat Pay"
},
"provider": {
"Access key": "Ключ доступа",
"Access key - Tooltip": "Ключ доступа - Подсказка",
@ -299,6 +344,7 @@
"Method": "Метод",
"Method - Tooltip": "Login behaviors, QR code or silent authorization",
"Name": "Наименование",
"New Provider": "New Provider",
"Parse": "Parse",
"Parse Metadata successfully": "Анализ метаданных успешно завершен",
"Port": "Порт",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "Изменить роль",
"New Role": "New Role",
"Sub roles": "Суб роли",
"Sub roles - Tooltip": "Суб роли - Tooltip",
"Sub users": "Субпользователи",
@ -381,8 +428,10 @@
"Please input your address!": "Пожалуйста, введите ваш адрес!",
"Please input your affiliation!": "Пожалуйста, введите вашу партнерство!",
"Please input your display name!": "Пожалуйста, введите ваше отображаемое имя!",
"Please input your personal name!": "Пожалуйста, введите ваше личное имя!",
"Please input your first name!": "Please input your first name!",
"Please input your last name!": "Please input your last name!",
"Please input your phone number!": "Пожалуйста, введите ваш номер телефона!",
"Please input your real name!": "Пожалуйста, введите ваше личное имя!",
"Please select your country/region!": "Пожалуйста, выберите вашу страну/регион!",
"Terms of Use": "Условия использования",
"The input is not valid Email!": "Ввод не является допустимым Email!",
@ -410,6 +459,7 @@
"Error text": "Error text",
"Error text - Tooltip": "Error text - Tooltip",
"Is hashed": "Хэшировано",
"New Syncer": "New Syncer",
"Sync interval": "Интервал синхронизации",
"Sync interval - Tooltip": "Интервал синхронизации - Tooltip",
"Table": "Таблица",
@ -424,6 +474,7 @@
"Authorization code": "Код авторизации",
"Edit Token": "Изменить токен",
"Expires in": "Истекает через",
"New Token": "New Token",
"Scope": "Сфера охвата",
"Token type": "Тип токена"
},
@ -462,6 +513,7 @@
"Modify password...": "Изменить пароль...",
"New Email": "Новое письмо",
"New Password": "Новый пароль",
"New User": "New User",
"New phone": "Новый телефон",
"OK": "ОК",
"Old Password": "Старый пароль",
@ -498,6 +550,7 @@
"Method": "Метод",
"Method - Tooltip": "Метод - Подсказка",
"Name": "Наименование",
"New Webhook": "New Webhook",
"URL": "URL",
"URL - Tooltip": "URL - Подсказка",
"Value": "Значение"

View File

@ -14,6 +14,9 @@
"Enable signup": "启用注册",
"Enable signup - Tooltip": "是否允许用户注册",
"File uploaded successfully": "文件上传成功",
"Grant types": "Grant types",
"Grant types - Tooltip": "选择允许哪些OAuth协议中的Grant types",
"New Application": "添加应用",
"Password ON": "开启密码",
"Password ON - Tooltip": "是否允许密码登录",
"Please select a HTML file": "请选择一个HTML文件",
@ -45,6 +48,7 @@
"Edit Cert": "编辑证书",
"Expire in years": "有效期(年)",
"Expire in years - Tooltip": "到期年份-工具提示",
"New Cert": "添加证书",
"Private key": "私钥",
"Private key - Tooltip": "私钥 - 工具提示",
"Private key copied to clipboard successfully": "私钥已成功复制到剪贴板",
@ -91,6 +95,7 @@
"Avatar": "头像",
"Avatar - Tooltip": "向其他人展示的头像",
"Back Home": "返回到首页",
"Cancel": "取消",
"Captcha": "人机验证码",
"Cert": "证书",
"Cert - Tooltip": "该应用所对应的客户端SDK需要验证的公钥证书",
@ -109,6 +114,7 @@
"Email": "电子邮箱",
"Email - Tooltip": "电子邮件:",
"Favicon - Tooltip": "网站的图标",
"First name": "名字",
"Forget URL": "忘记密码URL",
"Forget URL - Tooltip": "忘记密码URL",
"Home": "首页",
@ -119,6 +125,7 @@
"Is enabled - Tooltip": "是否启用",
"LDAPs": "LDAP",
"LDAPs - Tooltip": "LDAPs",
"Last name": "姓氏",
"Logo - Tooltip": "应用程序向外展示的图标",
"Master password": "万能密码",
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
@ -137,17 +144,18 @@
"Password type - Tooltip": "密码在数据库中存储的形式",
"Payments": "付款",
"Permissions": "权限",
"Personal name": "姓名",
"Phone": "手机号",
"Phone - Tooltip": "手机号",
"Phone prefix": "手机号前缀",
"Phone prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
"Preview": "预览",
"Preview - Tooltip": "预览",
"Products": "商品",
"Provider": "提供商",
"Provider - Tooltip": "第三方登录需要配置的提供方",
"Providers": "提供商",
"Providers - Tooltip": "第三方登录需要配置的提供方",
"Real name": "姓名",
"Records": "日志",
"Request URI": "请求URI",
"Resources": "资源",
@ -161,11 +169,14 @@
"Signup application": "注册应用",
"Signup application - Tooltip": "表示用户注册时通过哪个应用注册的",
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在",
"State": "状态",
"State - Tooltip": "状态",
"Swagger": "API文档",
"Syncers": "同步器",
"Timestamp": "时间戳",
"Tokens": "令牌",
"URL": "链接",
"URL - Tooltip": "URL链接",
"Up": "上移",
"User": "用户",
"User - Tooltip": "用户 - 工具提示",
@ -233,6 +244,7 @@
"Default avatar": "默认头像",
"Edit Organization": "编辑组织",
"Favicon": "图标",
"New Organization": "添加组织",
"Soft deletion": "软删除",
"Soft deletion - Tooltip": "启用后,删除用户信息时不会在数据库彻底清除,只会标记为已删除状态",
"Website URL": "网页地址",
@ -245,7 +257,8 @@
"Currency - Tooltip": "如USD美元CNY人民币等",
"Edit Payment": "编辑付款",
"Good": "商品",
"Good - Tooltip": "购买的商品名称"
"Good - Tooltip": "购买的商品名称",
"New Payment": "添加付款"
},
"permission": {
"Actions": "动作",
@ -253,10 +266,42 @@
"Edit Permission": "编辑权限",
"Effect": "效果",
"Effect - Tooltip": "允许还是拒绝",
"New Permission": "添加权限",
"Resource type": "资源类型",
"Resource type - Tooltip": "授权资源的类型",
"Resources": "资源"
},
"product": {
"Alipay": "支付宝",
"Buy Product": "购买商品",
"CNY": "人民币",
"Currency": "币种",
"Currency - Tooltip": "币种 - 工具提示",
"Detail": "详情",
"Detail - Tooltip": "详情 - 工具提示",
"Edit Product": "编辑商品",
"Image": "图片",
"Image - Tooltip": "图片 - 工具提示",
"New Product": "添加商品",
"Pay": "支付方式",
"Payment providers": "支付提供商",
"Payment providers - Tooltip": "支付提供商 - 工具提示",
"Paypal": "Paypal",
"Price": "价格",
"Price - Tooltip": "价格 - 工具提示",
"Quantity": "库存",
"Quantity - Tooltip": "库存 - 工具提示",
"SKU": "货号",
"Sold": "售出",
"Sold - Tooltip": "售出 - 工具提示",
"Tag": "类别",
"Tag - Tooltip": "类别 - 工具提示",
"Test buy page..": "测试购买页面..",
"There is no payment channel for this product.": "该商品没有付款方式。",
"This product is currently not in sale.": "该商品目前未在售。",
"USD": "美元",
"WeChat Pay": "微信支付"
},
"provider": {
"Access key": "访问密钥",
"Access key - Tooltip": "Access key",
@ -299,6 +344,7 @@
"Method": "方法",
"Method - Tooltip": "登录行为,二维码或者静默授权登录",
"Name": "名称",
"New Provider": "添加提供商",
"Parse": "Parse",
"Parse Metadata successfully": "解析元数据成功",
"Port": "端口",
@ -362,6 +408,7 @@
},
"role": {
"Edit Role": "编辑角色",
"New Role": "添加角色",
"Sub roles": "包含角色",
"Sub roles - Tooltip": "当前角色所包含的子角色",
"Sub users": "包含用户",
@ -381,8 +428,10 @@
"Please input your address!": "请输入您的地址!",
"Please input your affiliation!": "请输入您所在的工作单位!",
"Please input your display name!": "请输入您的显示名称!",
"Please input your personal name!": "请输入您的名!",
"Please input your first name!": "请输入您的名",
"Please input your last name!": "请输入您的姓氏!",
"Please input your phone number!": "请输入您的手机号码!",
"Please input your real name!": "请输入您的姓名!",
"Please select your country/region!": "请选择您的国家/地区",
"Terms of Use": "《用户协议》",
"The input is not valid Email!": "您输入的电子邮箱格式有误!",
@ -410,6 +459,7 @@
"Error text": "错误信息",
"Error text - Tooltip": "同步器连接数据库时发生的错误",
"Is hashed": "是否参与哈希计算",
"New Syncer": "添加同步器",
"Sync interval": "同步间隔",
"Sync interval - Tooltip": "单位为秒",
"Table": "表名",
@ -424,6 +474,7 @@
"Authorization code": "授权码",
"Edit Token": "编辑令牌",
"Expires in": "有效期",
"New Token": "添加令牌",
"Scope": "范围",
"Token type": "令牌类型"
},
@ -462,6 +513,7 @@
"Modify password...": "编辑密码...",
"New Email": "新邮箱",
"New Password": "新密码",
"New User": "添加用户",
"New phone": "新手机号",
"OK": "确定",
"Old Password": "旧密码",
@ -498,6 +550,7 @@
"Method": "方法",
"Method - Tooltip": "HTTP方法",
"Name": "名称",
"New Webhook": "添加Webhook",
"URL": "网址",
"URL - Tooltip": "URL",
"Value": "值"