mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-22 11:33:49 +08:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
22f5ad06ec | |||
18aa70dfb2 | |||
697b3e4998 | |||
d48d515c36 | |||
a5d166c35f | |||
4915963c52 | |||
759a1421e5 | |||
c14bf9fdab | |||
e19f07c521 | |||
39ab71c5db | |||
2c97f8a8b7 | |||
21392dcc14 | |||
953d3d5bc5 |
@ -54,7 +54,7 @@ m = (r.subOwner == p.subOwner || p.subOwner == "*") && \
|
||||
(r.urlPath == p.urlPath || p.urlPath == "*") && \
|
||||
(r.objOwner == p.objOwner || p.objOwner == "*") && \
|
||||
(r.objName == p.objName || p.objName == "*") || \
|
||||
(r.urlPath == "/api/update-user" && r.subOwner == r.objOwner && r.subName == r.objName)
|
||||
(r.subOwner == r.objOwner && r.subName == r.objName)
|
||||
`
|
||||
|
||||
m, err := model.NewModelFromString(modelText)
|
||||
|
@ -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)
|
||||
|
@ -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)}
|
||||
}
|
||||
|
116
controllers/product.go
Normal file
116
controllers/product.go
Normal 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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
130
object/product.go
Normal file
130
object/product.go
Normal 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)
|
||||
}
|
226
object/token.go
226
object/token.go
@ -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,149 @@ 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 token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge {
|
||||
return nil, errors.New("error: incorrect code_verifier")
|
||||
}
|
||||
|
||||
if application.ClientSecret != clientSecret {
|
||||
// when using PKCE, the Client Secret can be empty,
|
||||
// but if it is provided, it must be accurate.
|
||||
if token.CodeChallenge == "" {
|
||||
return nil, errors.New("error: invalid client_secret")
|
||||
} else {
|
||||
if 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 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
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/astaxie/beego/context"
|
||||
"github.com/casdoor/casdoor/authz"
|
||||
@ -57,6 +58,8 @@ func getSubject(ctx *context.Context) (string, string) {
|
||||
|
||||
func getObject(ctx *context.Context) (string, string) {
|
||||
method := ctx.Request.Method
|
||||
path := ctx.Request.URL.Path
|
||||
|
||||
if method == http.MethodGet {
|
||||
// query == "?id=built-in/admin"
|
||||
id := ctx.Input.Query("id")
|
||||
@ -78,6 +81,14 @@ func getObject(ctx *context.Context) (string, string) {
|
||||
//panic(err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if path == "/api/delete-resource" {
|
||||
tokens := strings.Split(obj.Name, "/")
|
||||
if len(tokens) >= 2 {
|
||||
obj.Name = tokens[len(tokens)-2]
|
||||
}
|
||||
}
|
||||
|
||||
return obj.Owner, obj.Name
|
||||
}
|
||||
}
|
||||
|
@ -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 != "" {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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,13 +434,20 @@ class App extends Component {
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
// res.push(
|
||||
// <Menu.Item key="/payments">
|
||||
// <Link to="/payments">
|
||||
// {i18next.t("general:Payments")}
|
||||
// </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">
|
||||
{i18next.t("general:Payments")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
res.push(
|
||||
<Menu.Item key="/swagger">
|
||||
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
|
||||
@ -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} />)}/>
|
||||
|
@ -61,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,
|
||||
});
|
||||
@ -163,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 => {
|
||||
@ -435,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"))} :
|
||||
@ -494,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} />
|
||||
@ -510,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>
|
||||
@ -525,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} />
|
||||
@ -538,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>
|
||||
@ -555,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>
|
||||
|
@ -113,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 => {
|
||||
@ -188,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 => {
|
||||
|
@ -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) {
|
||||
@ -105,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"))} :
|
||||
|
@ -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";
|
||||
|
196
web/src/ProductBuyPage.js
Normal file
196
web/src/ProductBuyPage.js
Normal 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
311
web/src/ProductEditPage.js
Normal 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")}
|
||||
<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
295
web/src/ProductListPage.js
Normal 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")}
|
||||
<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;
|
@ -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);
|
||||
|
@ -171,6 +171,7 @@ class SignupTable extends React.Component {
|
||||
options = [
|
||||
{id: 'None', name: 'None'},
|
||||
{id: 'Real name', name: 'Real name'},
|
||||
{id: 'First, last', name: 'First, last'},
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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}`);
|
||||
|
@ -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 === "Real name" ? i18next.t("general:Real 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 === "Real name" ? i18next.t("signup:Please input your real 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,
|
||||
},
|
||||
]}
|
||||
|
56
web/src/backend/ProductBackend.js
Normal file
56
web/src/backend/ProductBackend.js
Normal 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());
|
||||
}
|
@ -14,6 +14,8 @@
|
||||
"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",
|
||||
@ -112,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",
|
||||
@ -122,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",
|
||||
@ -146,6 +150,7 @@
|
||||
"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",
|
||||
@ -164,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",
|
||||
@ -263,6 +271,37 @@
|
||||
"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",
|
||||
@ -389,6 +428,8 @@
|
||||
"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 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!",
|
||||
|
@ -10,10 +10,12 @@
|
||||
"Edit Application": "Edit Application",
|
||||
"Enable code signin": "Enable code signin",
|
||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||
"Enable signin session - Tooltip": "Whether to preserve the session in Casdoor after login",
|
||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||
"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",
|
||||
@ -112,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",
|
||||
@ -122,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",
|
||||
@ -146,6 +150,7 @@
|
||||
"Phone prefix - Tooltip": "Phone prefix - Tooltip",
|
||||
"Preview": "Preview",
|
||||
"Preview - Tooltip": "Preview - Tooltip",
|
||||
"Products": "Products",
|
||||
"Provider": "Provider",
|
||||
"Provider - Tooltip": "Provider - Tooltip",
|
||||
"Providers": "Providers",
|
||||
@ -164,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",
|
||||
@ -263,6 +271,37 @@
|
||||
"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",
|
||||
@ -389,6 +428,8 @@
|
||||
"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 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!",
|
||||
|
@ -14,6 +14,8 @@
|
||||
"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",
|
||||
@ -112,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",
|
||||
@ -122,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",
|
||||
@ -146,6 +150,7 @@
|
||||
"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",
|
||||
@ -164,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",
|
||||
@ -263,6 +271,37 @@
|
||||
"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",
|
||||
@ -389,6 +428,8 @@
|
||||
"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 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!",
|
||||
|
@ -14,6 +14,8 @@
|
||||
"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",
|
||||
@ -112,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": "ホーム",
|
||||
@ -122,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": "マスターパスワード - ツールチップ",
|
||||
@ -146,6 +150,7 @@
|
||||
"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": "プロバイダー",
|
||||
@ -164,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": "ユーザー → ツールチップ",
|
||||
@ -263,6 +271,37 @@
|
||||
"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": "アクセスキー → ツールチップ",
|
||||
@ -389,6 +428,8 @@
|
||||
"Please input your address!": "住所を入力してください!",
|
||||
"Please input your affiliation!": "所属を入力してください!",
|
||||
"Please input your display 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!": "あなたの国/地域を選択してください!",
|
||||
|
@ -14,6 +14,8 @@
|
||||
"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",
|
||||
@ -112,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",
|
||||
@ -122,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",
|
||||
@ -146,6 +150,7 @@
|
||||
"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",
|
||||
@ -164,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",
|
||||
@ -263,6 +271,37 @@
|
||||
"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",
|
||||
@ -389,6 +428,8 @@
|
||||
"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 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!",
|
||||
|
@ -14,6 +14,8 @@
|
||||
"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",
|
||||
@ -112,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": "Домашний",
|
||||
@ -122,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",
|
||||
@ -146,6 +150,7 @@
|
||||
"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": "Поставщики",
|
||||
@ -164,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": "Пользователь - Подсказка",
|
||||
@ -263,6 +271,37 @@
|
||||
"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": "Ключ доступа - Подсказка",
|
||||
@ -389,6 +428,8 @@
|
||||
"Please input your address!": "Пожалуйста, введите ваш адрес!",
|
||||
"Please input your affiliation!": "Пожалуйста, введите вашу партнерство!",
|
||||
"Please input your display 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!": "Пожалуйста, выберите вашу страну/регион!",
|
||||
|
@ -14,6 +14,8 @@
|
||||
"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": "是否允许密码登录",
|
||||
@ -112,6 +114,7 @@
|
||||
"Email": "电子邮箱",
|
||||
"Email - Tooltip": "电子邮件:",
|
||||
"Favicon - Tooltip": "网站的图标",
|
||||
"First name": "名字",
|
||||
"Forget URL": "忘记密码URL",
|
||||
"Forget URL - Tooltip": "忘记密码URL",
|
||||
"Home": "首页",
|
||||
@ -122,6 +125,7 @@
|
||||
"Is enabled - Tooltip": "是否启用",
|
||||
"LDAPs": "LDAP",
|
||||
"LDAPs - Tooltip": "LDAPs",
|
||||
"Last name": "姓氏",
|
||||
"Logo - Tooltip": "应用程序向外展示的图标",
|
||||
"Master password": "万能密码",
|
||||
"Master password - Tooltip": "可用来登录该组织下的所有用户,方便管理员以该用户身份登录,以解决技术问题",
|
||||
@ -146,6 +150,7 @@
|
||||
"Phone prefix - Tooltip": "移动电话号码前缀,用于区分国家或地区",
|
||||
"Preview": "预览",
|
||||
"Preview - Tooltip": "预览",
|
||||
"Products": "商品",
|
||||
"Provider": "提供商",
|
||||
"Provider - Tooltip": "第三方登录需要配置的提供方",
|
||||
"Providers": "提供商",
|
||||
@ -164,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": "用户 - 工具提示",
|
||||
@ -263,6 +271,37 @@
|
||||
"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",
|
||||
@ -389,6 +428,8 @@
|
||||
"Please input your address!": "请输入您的地址!",
|
||||
"Please input your affiliation!": "请输入您所在的工作单位!",
|
||||
"Please input your display 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!": "请选择您的国家/地区",
|
||||
|
Reference in New Issue
Block a user