mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-17 04:03:23 +08:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
e78ea2546f | |||
f7705931f7 | |||
5d8b710bf7 | |||
b85ad896bf | |||
42c2210178 | |||
d52caed3a9 | |||
27d8cd758d | |||
98f77960de | |||
e5b71a08ae | |||
3ad4b7a43c | |||
c5c3a08aa9 | |||
8efd964835 | |||
5dac87a4c3 | |||
49c3266400 | |||
39548d5d72 | |||
1c949e415e | |||
1b840a2e9f | |||
c9849d8b55 | |||
b747f5e27c | |||
8b340105c1 | |||
43b1006f11 | |||
78efc9c2d0 | |||
c4089eacb7 | |||
4acba2d493 | |||
fc0ca4cceb | |||
912d9d0c01 | |||
8e48bddf5f | |||
c05fb77224 | |||
9af9ead939 | |||
f5590c42f7 | |||
5597f99e3c | |||
ea005aaf4d | |||
e5c1f560c5 | |||
20fc7d1b58 | |||
cf3b46130b | |||
cab51fae9c | |||
b867872da4 | |||
305867f49a | |||
3f90c18a19 | |||
9e5a64c021 | |||
4263af6f2c | |||
3e92d761b9 | |||
0e41568f62 | |||
fb7e2729c6 | |||
28b9154d7e | |||
b0b3eb0805 | |||
73bd9dd517 | |||
0bc8c2d15f | |||
7b78e60265 | |||
7464f9a8ad | |||
d3a7a062d3 | |||
67a0264411 | |||
a6a055cc83 | |||
a89a7f9eb7 | |||
287f60353c | |||
530330bd66 | |||
70a1428972 |
152
README.md
152
README.md
@ -42,166 +42,66 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## Online demo
|
||||
|
||||
Deployed site: https://door.casdoor.com/
|
||||
- International: https://door.casdoor.org (read-only)
|
||||
- Asian mirror: https://door.casdoor.com (read-only)
|
||||
- Asian mirror: https://demo.casdoor.com (read-write, will restore for every 5 minutes)
|
||||
|
||||
## Quick Start
|
||||
Run your own casdoor program in a few minutes.
|
||||
|
||||
### Download
|
||||
|
||||
There are two methods, get code via go subcommand `get`:
|
||||
## Documentation
|
||||
|
||||
```shell
|
||||
go get github.com/casdoor/casdoor
|
||||
```
|
||||
- International: https://casdoor.org
|
||||
- Asian mirror: https://docs.casdoor.cn
|
||||
|
||||
or `git`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/casdoor/casdoor
|
||||
```
|
||||
|
||||
Finally, change directory:
|
||||
## Install
|
||||
|
||||
```bash
|
||||
cd casdoor/
|
||||
```
|
||||
- By source code: https://casdoor.org/docs/basic/server-installation
|
||||
- By Docker: https://casdoor.org/docs/basic/try-with-docker
|
||||
|
||||
We provide two start up methods for all kinds of users.
|
||||
|
||||
### Manual
|
||||
|
||||
#### Simple configuration
|
||||
Casdoor requires a running Relational database to be operational.Thus you need to modify configuration to point out the location of database.
|
||||
## How to connect to Casdoor?
|
||||
|
||||
Edit `conf/app.conf`, modify `dataSourceName` to correct database info, which follows this format:
|
||||
https://casdoor.org/docs/how-to-connect/overview
|
||||
|
||||
```bash
|
||||
username:password@tcp(database_ip:database_port)/
|
||||
```
|
||||
|
||||
Then create an empty schema (database) named `casdoor` in your relational database. After the program runs for the first time, it will automatically create tables in this schema.
|
||||
|
||||
You can also edit `main.go`, modify `false` to `true`. It will automatically create the schema (database) named `casdoor` in this database.
|
||||
## Casdoor Public API
|
||||
|
||||
```bash
|
||||
createDatabase := flag.Bool("createDatabase", false, "true if you need casdoor to create database")
|
||||
```
|
||||
- Docs: https://casdoor.org/docs/basic/public-api
|
||||
- Swagger: https://door.casdoor.com/swagger
|
||||
|
||||
#### Run
|
||||
|
||||
Casdoor provides two run modes, the difference is binary size and user prompt.
|
||||
|
||||
##### Dev Mode
|
||||
## Integrations
|
||||
|
||||
Edit `conf/app.conf`, set `runmode=dev`. Firstly build front-end files:
|
||||
https://casdoor.org/docs/integration/apisix
|
||||
|
||||
```bash
|
||||
cd web/ && yarn && yarn run start
|
||||
```
|
||||
*❗ A word of caution ❗: Casdoor's front-end is built using yarn. You should use `yarn` instead of `npm`. It has a potential failure during building the files if you use `npm`.*
|
||||
|
||||
Then build back-end binary file, change directory to root(Relative to casdoor):
|
||||
## How to contact?
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
- Gitter: https://gitter.im/casbin/casdoor
|
||||
- Forum: https://forum.casbin.com
|
||||
- Contact: https://tawk.to/chat/623352fea34c2456412b8c51/1fuc7od6e
|
||||
|
||||
That's it! Try to visit http://127.0.0.1:7001/. :small_airplane:
|
||||
**But make sure you always request the backend port 8000 when you are using SDKs.**
|
||||
|
||||
##### Production Mode
|
||||
|
||||
Edit `conf/app.conf`, set `runmode=prod`. Firstly build front-end files:
|
||||
|
||||
```bash
|
||||
cd web/ && yarn && yarn run build
|
||||
```
|
||||
|
||||
Then build back-end binary file, change directory to root(Relative to casdoor):
|
||||
|
||||
```bash
|
||||
go build main.go && sudo ./main
|
||||
```
|
||||
|
||||
> Notice, you should visit back-end port, default 8000. Now try to visit **http://SERVER_IP:8000/**
|
||||
|
||||
### Docker
|
||||
|
||||
Casdoor provide 2 kinds of image:
|
||||
- casbin/casdoor-all-in-one, in which casdoor binary, a mysql database and all necessary configurations are packed up. This image is for new user to have a trial on casdoor quickly. **With this image you can start a casdoor immediately with one single command (or two) without any complex configuration**. **Note: we DO NOT recommend you to use this image in productive environment**
|
||||
|
||||
- casbin/casdoor: normal & graceful casdoor image with only casdoor and environment installed.
|
||||
|
||||
This method requires [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) to be installed first.
|
||||
|
||||
### Start casdoor with casbin/casdoor-all-in-one
|
||||
if the image is not pulled, pull it from dockerhub
|
||||
```shell
|
||||
docker pull casbin/casdoor-all-in-one
|
||||
```
|
||||
Start it with
|
||||
```shell
|
||||
docker run -p 8000:8000 casbin/casdoor-all-in-one
|
||||
```
|
||||
Now you can visit http://localhost:8000 and have a try. Default account and password is 'admin' and '123'. Go for it!
|
||||
|
||||
### Start casdoor with casbin/casdoor
|
||||
#### modify the configurations
|
||||
For the convenience of your first attempt, docker-compose.yml contains commands to start a database via docker.
|
||||
|
||||
Thus edit `conf/app.conf` to point out the location of database(db:3306), modify `dataSourceName` to the fixed content:
|
||||
|
||||
```bash
|
||||
dataSourceName = root:123456@tcp(db:3306)/
|
||||
```
|
||||
|
||||
> If you need to modify `conf/app.conf`, you need to re-run `docker-compose up`.
|
||||
|
||||
#### Run
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### K8S
|
||||
You could use helm to deploy casdoor in k8s. At first, you should modify the [configmap](./manifests/casdoor/templates/configmap.yaml) for your application.
|
||||
And then run bellow command to deploy it.
|
||||
|
||||
```bash
|
||||
IMG_TAG=latest make deploy
|
||||
```
|
||||
|
||||
And undeploy it with:
|
||||
```bash
|
||||
make undeploy
|
||||
```
|
||||
|
||||
That's it! Try to visit http://localhost:8000/. :small_airplane:
|
||||
|
||||
## Detailed documentation
|
||||
|
||||
We also provide a complete [document](https://casdoor.org/) as a reference.
|
||||
|
||||
## Other examples
|
||||
|
||||
These all use casdoor as a centralized authentication platform.
|
||||
|
||||
- [Casnode](https://github.com/casbin/casnode): Next-generation forum software based on React + Golang.
|
||||
- [Casbin-OA](https://github.com/casbin/casbin-oa): A full-featured OA(Office Assistant) system.
|
||||
- ......
|
||||
|
||||
## Contribute
|
||||
|
||||
For casdoor, if you have any questions, you can give Issues, or you can also directly start Pull Requests(but we recommend giving issues first to communicate with the community).
|
||||
|
||||
### I18n notice
|
||||
### I18n translation
|
||||
|
||||
If you are contributing to casdoor, please note that we use [Crowdin](https://crowdin.com/project/casdoor-web) as translating platform and i18next as translating tool. When you add some words using i18next in the ```web/``` directory, please remember to add what you have added to the ```web/src/locales/en/data.json``` file.
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)
|
||||
|
||||
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)
|
||||
|
@ -80,16 +80,17 @@ p, *, *, GET, /api/get-app-login, *, *
|
||||
p, *, *, POST, /api/logout, *, *
|
||||
p, *, *, GET, /api/get-account, *, *
|
||||
p, *, *, GET, /api/userinfo, *, *
|
||||
p, *, *, POST, /api/login/oauth/access_token, *, *
|
||||
p, *, *, POST, /api/login/oauth/refresh_token, *, *
|
||||
p, *, *, GET, /api/login/oauth/logout, *, *
|
||||
p, *, *, *, /api/login/oauth, *, *
|
||||
p, *, *, GET, /api/get-application, *, *
|
||||
p, *, *, GET, /api/get-applications, *, *
|
||||
p, *, *, GET, /api/get-user, *, *
|
||||
p, *, *, GET, /api/get-user-application, *, *
|
||||
p, *, *, GET, /api/get-resources, *, *
|
||||
p, *, *, GET, /api/get-product, *, *
|
||||
p, *, *, POST, /api/buy-product, *, *
|
||||
p, *, *, GET, /api/get-payment, *, *
|
||||
p, *, *, POST, /api/update-payment, *, *
|
||||
p, *, *, POST, /api/invoice-payment, *, *
|
||||
p, *, *, GET, /api/get-providers, *, *
|
||||
p, *, *, POST, /api/unlink, *, *
|
||||
p, *, *, POST, /api/set-password, *, *
|
||||
|
8
build.sh
8
build.sh
@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
#try to connect to google to determine whether user need to use proxy
|
||||
curl www.google.com -o /dev/null --connect-timeout 5
|
||||
curl www.google.com -o /dev/null --connect-timeout 5 2 > /dev/null
|
||||
if [ $? == 0 ]
|
||||
then
|
||||
echo "connect to google.com successed"
|
||||
echo "Successfully connected to Google, no need to use Go proxy"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
|
||||
else
|
||||
echo "connect to google.com failed"
|
||||
echo "Google is blocked, Go proxy is enabled: GOPROXY=https://goproxy.cn,direct"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server .
|
||||
fi
|
||||
fi
|
||||
|
@ -116,7 +116,7 @@ func (c *ApiController) Signup() {
|
||||
return
|
||||
}
|
||||
|
||||
if application.IsSignupItemVisible("Email") && form.Email != "" {
|
||||
if application.IsSignupItemVisible("Email") && application.GetSignupItemRule("Email") != "No verification" && form.Email != "" {
|
||||
checkResult := object.CheckVerificationCode(form.Email, form.EmailCode)
|
||||
if len(checkResult) != 0 {
|
||||
c.ResponseError(fmt.Sprintf("Email: %s", checkResult))
|
||||
@ -210,7 +210,7 @@ func (c *ApiController) Signup() {
|
||||
record := object.NewRecord(c.Ctx)
|
||||
record.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
go object.AddRecord(record)
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", user.Owner, user.Name)
|
||||
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
|
||||
@ -285,6 +285,7 @@ func (c *ApiController) GetUserinfo() {
|
||||
resp, err := object.GetUserInfo(userId, scope, aud, host)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = resp
|
||||
c.ServeJSON()
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func codeToResponse(code *object.Code) *Response {
|
||||
@ -133,7 +134,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
|
||||
// @Param scope query string true "scope"
|
||||
// @Param state query string true "state"
|
||||
// @Success 200 {object} controllers.api_controller.Response The Response object
|
||||
// @router /update-application [get]
|
||||
// @router /get-app-login [get]
|
||||
func (c *ApiController) GetApplicationLogin() {
|
||||
clientId := c.Input().Get("clientId")
|
||||
responseType := c.Input().Get("responseType")
|
||||
@ -222,7 +223,11 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
// disable the verification code
|
||||
object.DisableVerificationCode(form.Username)
|
||||
if strings.Contains(form.Username, "@") {
|
||||
object.DisableVerificationCode(form.Username)
|
||||
} else {
|
||||
object.DisableVerificationCode(fmt.Sprintf("+%s%s", form.PhonePrefix, form.Username))
|
||||
}
|
||||
|
||||
user = object.GetUserByFields(form.Organization, form.Username)
|
||||
if user == nil {
|
||||
@ -248,7 +253,7 @@ func (c *ApiController) Login() {
|
||||
record := object.NewRecord(c.Ctx)
|
||||
record.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
go object.AddRecord(record)
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
}
|
||||
} else if form.Provider != "" {
|
||||
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
|
||||
@ -283,7 +288,7 @@ func (c *ApiController) Login() {
|
||||
clientSecret = provider.ClientSecret2
|
||||
}
|
||||
|
||||
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain)
|
||||
idProvider := idp.GetIdProvider(provider.Type, provider.SubType, clientId, clientSecret, provider.AppId, form.RedirectUri, provider.Domain, provider.CustomAuthUrl, provider.CustomTokenUrl, provider.CustomUserInfoUrl)
|
||||
if idProvider == nil {
|
||||
c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type))
|
||||
return
|
||||
@ -341,7 +346,7 @@ func (c *ApiController) Login() {
|
||||
record := object.NewRecord(c.Ctx)
|
||||
record.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
go object.AddRecord(record)
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
} else if provider.Category == "OAuth" {
|
||||
// Sign up via OAuth
|
||||
if !application.EnableSignUp {
|
||||
@ -354,6 +359,19 @@ func (c *ApiController) Login() {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle username conflicts
|
||||
tmpUser := object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Username))
|
||||
if tmpUser != nil {
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
uidStr := strings.Split(uid.String(), "-")
|
||||
userInfo.Username = fmt.Sprintf("%s_%s", userInfo.Username, uidStr[1])
|
||||
}
|
||||
|
||||
properties := map[string]string{}
|
||||
properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2)
|
||||
user = &object.User{
|
||||
@ -390,7 +408,7 @@ func (c *ApiController) Login() {
|
||||
record := object.NewRecord(c.Ctx)
|
||||
record.Organization = application.Organization
|
||||
record.User = user.Name
|
||||
go object.AddRecord(record)
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
} else if provider.Category == "SAML" {
|
||||
resp = &Response{Status: "error", Msg: "The account does not exist"}
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ func (c *ApiController) SyncLdapUsers() {
|
||||
|
||||
object.UpdateLdapSyncTime(ldapId)
|
||||
|
||||
exist, failed := object.SyncLdapUsers(owner, users)
|
||||
exist, failed := object.SyncLdapUsers(owner, users, ldapId)
|
||||
c.Data["json"] = &Response{Status: "ok", Data: &LdapSyncResp{
|
||||
Exist: *exist,
|
||||
Failed: *failed,
|
||||
|
120
controllers/model.go
Normal file
120
controllers/model.go
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/astaxie/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetModels
|
||||
// @Title GetModels
|
||||
// @Tag Model API
|
||||
// @Description get models
|
||||
// @Param owner query string true "The owner of models"
|
||||
// @Success 200 {array} object.Model The Response object
|
||||
// @router /get-models [get]
|
||||
func (c *ApiController) GetModels() {
|
||||
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.GetModels(owner)
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, int64(object.GetModelCount(owner, field, value)))
|
||||
models := object.GetPaginationModels(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
c.ResponseOk(models, paginator.Nums())
|
||||
}
|
||||
}
|
||||
|
||||
// GetModel
|
||||
// @Title GetModel
|
||||
// @Tag Model API
|
||||
// @Description get model
|
||||
// @Param id query string true "The id of the model"
|
||||
// @Success 200 {object} object.Model The Response object
|
||||
// @router /get-model [get]
|
||||
func (c *ApiController) GetModel() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
c.Data["json"] = object.GetModel(id)
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateModel
|
||||
// @Title UpdateModel
|
||||
// @Tag Model API
|
||||
// @Description update model
|
||||
// @Param id query string true "The id of the model"
|
||||
// @Param body body object.Model true "The details of the model"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-model [post]
|
||||
func (c *ApiController) UpdateModel() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var model object.Model
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateModel(id, &model))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddModel
|
||||
// @Title AddModel
|
||||
// @Tag Model API
|
||||
// @Description add model
|
||||
// @Param body body object.Model true "The details of the model"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-model [post]
|
||||
func (c *ApiController) AddModel() {
|
||||
var model object.Model
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddModel(&model))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteModel
|
||||
// @Title DeleteModel
|
||||
// @Tag Model API
|
||||
// @Description delete model
|
||||
// @Param body body object.Model true "The details of the model"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-model [post]
|
||||
func (c *ApiController) DeleteModel() {
|
||||
var model object.Model
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteModel(&model))
|
||||
c.ServeJSON()
|
||||
}
|
@ -158,3 +158,20 @@ func (c *ApiController) NotifyPayment() {
|
||||
panic(fmt.Errorf("NotifyPayment() failed: %v", ok))
|
||||
}
|
||||
}
|
||||
|
||||
// @Title InvoicePayment
|
||||
// @Tag Payment API
|
||||
// @Description invoice payment
|
||||
// @Param id query string true "The id of the payment"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /invoice-payment [post]
|
||||
func (c *ApiController) InvoicePayment() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
payment := object.GetPayment(id)
|
||||
invoiceUrl, err := object.InvoicePayment(payment)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
}
|
||||
c.ResponseOk(invoiceUrl)
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ func (c *ApiController) GetSamlMeta() {
|
||||
application := object.GetApplication(paramApp)
|
||||
if application == nil {
|
||||
c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp))
|
||||
return
|
||||
}
|
||||
metadata, _ := object.GetSamlMeta(application, host)
|
||||
c.Data["xml"] = metadata
|
||||
|
@ -16,7 +16,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/astaxie/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -114,3 +113,18 @@ func (c *ApiController) DeleteSyncer() {
|
||||
c.Data["json"] = wrapActionResponse(object.DeleteSyncer(&syncer))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title RunSyncer
|
||||
// @Tag Syncer API
|
||||
// @Description run syncer
|
||||
// @Param body body object.Syncer true "The details of the syncer"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /run-syncer [get]
|
||||
func (c *ApiController) RunSyncer() {
|
||||
id := c.Input().Get("id")
|
||||
syncer := object.GetSyncer(id)
|
||||
|
||||
object.RunSyncer(syncer)
|
||||
|
||||
c.ResponseOk()
|
||||
}
|
||||
|
@ -230,7 +230,7 @@ func (c *ApiController) RefreshToken() {
|
||||
clientSecret = tokenRequest.ClientSecret
|
||||
grantType = tokenRequest.GrantType
|
||||
scope = tokenRequest.Scope
|
||||
|
||||
refreshToken = tokenRequest.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,21 +275,20 @@ func (c *ApiController) IntrospectToken() {
|
||||
tokenValue := c.Input().Get("token")
|
||||
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
|
||||
if !ok {
|
||||
util.LogWarning(c.Ctx, "Basic Authorization parses failed")
|
||||
c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
clientId = c.Input().Get("client_id")
|
||||
clientSecret = c.Input().Get("client_secret")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
c.ResponseError("empty clientId or clientSecret")
|
||||
return
|
||||
}
|
||||
}
|
||||
application := object.GetApplicationByClientId(clientId)
|
||||
if application == nil || application.ClientSecret != clientSecret {
|
||||
util.LogWarning(c.Ctx, "Basic Authorization failed")
|
||||
c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"}
|
||||
c.ServeJSON()
|
||||
c.ResponseError("invalid application or wrong clientSecret")
|
||||
return
|
||||
}
|
||||
token := object.GetTokenByTokenAndApplication(tokenValue, application.Name)
|
||||
if token == nil {
|
||||
util.LogWarning(c.Ctx, "application: %s can not find token", application.Name)
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
@ -299,7 +298,6 @@ func (c *ApiController) IntrospectToken() {
|
||||
// and token revoked case. but we not implement
|
||||
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
|
||||
// refs: https://tools.ietf.org/html/rfc7009
|
||||
util.LogWarning(c.Ctx, "token invalid")
|
||||
c.Data["json"] = &object.IntrospectionResponse{Active: false}
|
||||
c.ServeJSON()
|
||||
return
|
||||
|
@ -25,4 +25,5 @@ type TokenRequest struct {
|
||||
Password string `json:"password"`
|
||||
Tag string `json:"tag"`
|
||||
Avatar string `json:"avatar"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
@ -87,6 +87,17 @@ func (c *ApiController) GetUser() {
|
||||
id := c.Input().Get("id")
|
||||
owner := c.Input().Get("owner")
|
||||
email := c.Input().Get("email")
|
||||
userOwner, _ := util.GetOwnerAndNameFromId(id)
|
||||
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", userOwner))
|
||||
|
||||
if !organization.IsProfilePublic {
|
||||
requestUserId := c.GetSessionUsername()
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, id, false)
|
||||
if !hasPermission {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var user *object.User
|
||||
if email == "" {
|
||||
@ -233,39 +244,15 @@ func (c *ApiController) SetPassword() {
|
||||
newPassword := c.Ctx.Request.Form.Get("newPassword")
|
||||
|
||||
requestUserId := c.GetSessionUsername()
|
||||
if requestUserId == "" {
|
||||
c.ResponseError("Please login first")
|
||||
return
|
||||
}
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", userOwner, userName)
|
||||
targetUser := object.GetUser(userId)
|
||||
if targetUser == nil {
|
||||
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId))
|
||||
|
||||
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true)
|
||||
if !hasPermission {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission := false
|
||||
if strings.HasPrefix(requestUserId, "app/") {
|
||||
hasPermission = true
|
||||
} else {
|
||||
requestUser := object.GetUser(requestUserId)
|
||||
if requestUser == nil {
|
||||
c.ResponseError("Session outdated. Please login again.")
|
||||
return
|
||||
}
|
||||
if requestUser.IsGlobalAdmin {
|
||||
hasPermission = true
|
||||
} else if requestUserId == userId {
|
||||
hasPermission = true
|
||||
} else if targetUser.Owner == requestUser.Owner && requestUser.IsAdmin {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
if !hasPermission {
|
||||
c.ResponseError("You don't have the permission to do this.")
|
||||
return
|
||||
}
|
||||
targetUser := object.GetUser(userId)
|
||||
|
||||
if oldPassword != "" {
|
||||
msg := object.CheckPassword(targetUser, oldPassword)
|
||||
|
38
cred/argon2id.go
Normal file
38
cred/argon2id.go
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 cred
|
||||
|
||||
import "github.com/alexedwards/argon2id"
|
||||
|
||||
type Argon2idCredManager struct{}
|
||||
|
||||
func NewArgon2idCredManager() *Argon2idCredManager {
|
||||
cm := &Argon2idCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *Argon2idCredManager) GetHashedPassword(password string, userSalt string, organizationSalt string) string {
|
||||
|
||||
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
func (cm *Argon2idCredManager) IsPasswordCorrect(plainPwd string, hashedPwd string, userSalt string, organizationSalt string) bool {
|
||||
match, _ := argon2id.ComparePasswordAndHash(plainPwd, hashedPwd)
|
||||
return match
|
||||
}
|
@ -30,6 +30,8 @@ func GetCredManager(passwordType string) CredManager {
|
||||
return NewBcryptCredManager()
|
||||
} else if passwordType == "pbkdf2-salt" {
|
||||
return NewPbkdf2SaltCredManager()
|
||||
} else if passwordType == "argon2id" {
|
||||
return NewArgon2idCredManager()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ func getMd5HexDigest(s string) string {
|
||||
return res
|
||||
}
|
||||
|
||||
func NewMd5UserSaltCredManager() *Sha256SaltCredManager {
|
||||
cm := &Sha256SaltCredManager{}
|
||||
func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
|
||||
cm := &Md5UserSaltCredManager{}
|
||||
return cm
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ services:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
entrypoint: /bin/sh -c './server --createDatabase=true'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
|
13
go.mod
13
go.mod
@ -4,15 +4,15 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible // indirect
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/astaxie/beego v1.12.3
|
||||
github.com/aws/aws-sdk-go v1.37.30
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.4
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/casbin/casbin/v2 v2.30.1
|
||||
github.com/casbin/xorm-adapter/v2 v2.5.1
|
||||
github.com/casdoor/go-sms-sender v0.2.0
|
||||
github.com/casdoor/goth v1.69.0-FIX1
|
||||
github.com/casdoor/oss v1.2.0
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
@ -20,12 +20,10 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/jinzhu/configor v1.2.1 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/lestrrat-go/jwx v0.9.0
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0
|
||||
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/gosaml2 v0.6.0
|
||||
github.com/russellhaering/goxmldsig v1.1.1
|
||||
@ -35,14 +33,13 @@ require (
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
xorm.io/core v0.7.2
|
||||
xorm.io/xorm v1.0.3
|
||||
xorm.io/xorm v1.0.4
|
||||
)
|
||||
|
76
go.sum
76
go.sum
@ -35,6 +35,21 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
@ -50,18 +65,20 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 h1:loy0fjI90vF44BPW4ZYOkE3tDkGTy7yHURusOJimt+I=
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387/go.mod h1:GuR5j/NW7AU7tDAQUDGCtpiPxWIOy/c3kiRDnlwiCHc=
|
||||
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075 h1:Z0SzZttfYI/raZ5O9WF3cezZJTSW4Yz4Kow9uWdyRwg=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible h1:Ft+KeWIJxFP76LqgJbvtOA1qBIoC8vGkTV3QeCOeJC4=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ=
|
||||
github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.37.30 h1:fZeVg3QuTkWE/dEvPQbK6AL32+3G9ofJfGFSPS1XLH0=
|
||||
github.com/aws/aws-sdk-go v1.37.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.44.4 h1:ePN0CVJMdiz2vYUcJH96eyxRrtKGSDMgyhP6rah2OgE=
|
||||
github.com/aws/aws-sdk-go v1.44.4/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
|
||||
@ -85,6 +102,8 @@ github.com/casdoor/go-sms-sender v0.2.0 h1:52bin4EBOPzOee64s9UK7jxd22FODvT9/+Y/Z
|
||||
github.com/casdoor/go-sms-sender v0.2.0/go.mod h1:fsZsNnALvFIo+HFcE1U/oCQv4ZT42FdglXKMsEm3WSk=
|
||||
github.com/casdoor/goth v1.69.0-FIX1 h1:24Y3tfaJxWGJbxickGe3F9y2c8X1PgsQynhxGXV1f9Q=
|
||||
github.com/casdoor/goth v1.69.0-FIX1/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s=
|
||||
github.com/casdoor/oss v1.2.0 h1:ozLAE+nnNdFQBWbzH8U9spzaO8h8NrB57lBcdyMUUQ8=
|
||||
github.com/casdoor/oss v1.2.0/go.mod h1:qii35VBuxnR/uEuYSKpS0aJ8htQFOcCVsZ4FHgHLuss=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@ -113,6 +132,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||
@ -130,6 +151,12 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-pay/gopay v1.5.72 h1:3zm64xMBhJBa8rXbm//q5UiGgOa4WO5XYEnU394N2Zw=
|
||||
github.com/go-pay/gopay v1.5.72/go.mod h1:0qOGIJuFW7PKDOjmecwKyW0mgsVImgwB9yPJj0ilpn8=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
@ -252,18 +279,19 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lestrrat-go/jwx v0.9.0 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM=
|
||||
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c h1:UPJygtyk491bJJ/DnRJFuzcq9Dl9NSeFrJ7VdiRzMxc=
|
||||
github.com/ma314smith/signedxml v0.0.0-20210628192057-abc5b481ae1c/go.mod h1:KEgVcb43+f5KFUH/x6Vd3NROG0AIL2CuKMrIqYsmx6E=
|
||||
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
|
||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
@ -317,8 +345,9 @@ github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFB
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0 h1:q0y8TPa/sTwtriJPRe8gWL++PuZ+XbOUuvKU+hvtTYs=
|
||||
github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQXHqjEGJz1+U1a6yR5wA=
|
||||
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 h1:J2Xj92efYLxPl3BiibgEDEUiMsCBzwTurE/8JjD8CG4=
|
||||
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0=
|
||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||
github.com/qiniu/go-sdk/v7 v7.12.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@ -387,6 +416,11 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954 h1:BkypuErRT9A9I/iljuaG3/zdMjd/J6m8tKKJQtGfSdA=
|
||||
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -435,6 +469,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@ -451,9 +486,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -470,6 +507,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -484,6 +522,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -503,24 +542,28 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@ -696,5 +739,6 @@ xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
|
||||
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw=
|
||||
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
|
||||
xorm.io/xorm v1.0.3 h1:3dALAohvINu2mfEix5a5x5ZmSVGSljinoSGgvGbaZp0=
|
||||
xorm.io/xorm v1.0.3/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
xorm.io/xorm v1.0.4 h1:UBXA4I3NhiyjXfPqxXUkS2t5hMta9SSPATeMMaZg9oA=
|
||||
xorm.io/xorm v1.0.4/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
|
221
idp/bilibili.go
Normal file
221
idp/bilibili.go
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type BilibiliIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
}
|
||||
|
||||
func NewBilibiliIdProvider(clientId string, clientSecret string, redirectUrl string) *BilibiliIdProvider {
|
||||
idp := &BilibiliIdProvider{}
|
||||
|
||||
config := idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
idp.Config = config
|
||||
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *BilibiliIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
|
||||
func (idp *BilibiliIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
|
||||
var endpoint = oauth2.Endpoint{
|
||||
TokenURL: "https://api.bilibili.com/x/account-oauth2/v1/token",
|
||||
AuthURL: "http://member.bilibili.com/arcopen/fn/user/account/info",
|
||||
}
|
||||
|
||||
var config = &oauth2.Config{
|
||||
Scopes: []string{"", ""},
|
||||
Endpoint: endpoint,
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectUrl,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
type BilibiliProviderToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type BilibiliIdProviderTokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
TTL int `json:"ttl"`
|
||||
Data BilibiliProviderToken `json:"data"`
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"code": 0,
|
||||
"message": "0",
|
||||
"ttl": 1,
|
||||
"data": {
|
||||
"access_token": "d30bedaa4d8eb3128cf35ddc1030e27d",
|
||||
"expires_in": 1630220614,
|
||||
"refresh_token": "WxFDKwqScZIQDm4iWmKDvetyFugM6HkX"
|
||||
}
|
||||
}
|
||||
*/
|
||||
// GetToken use code get access_token (*operation of getting code ought to be done in front)
|
||||
// get more detail via: https://openhome.bilibili.com/doc/4/eaf0e2b5-bde9-b9a0-9be1-019bb455701c
|
||||
func (idp *BilibiliIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
pTokenParams := &struct {
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code string `json:"code"`
|
||||
}{
|
||||
idp.Config.ClientID,
|
||||
idp.Config.ClientSecret,
|
||||
"authorization_code",
|
||||
code,
|
||||
}
|
||||
|
||||
data, err := idp.postWithBody(pTokenParams, idp.Config.Endpoint.TokenURL)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &BilibiliIdProviderTokenResponse{}
|
||||
err = json.Unmarshal(data, response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.Code != 0 {
|
||||
return nil, fmt.Errorf("pToken.Errcode = %d, pToken.Errmsg = %s", response.Code, response.Message)
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: response.Data.AccessToken,
|
||||
Expiry: time.Unix(time.Now().Unix()+int64(response.Data.ExpiresIn), 0),
|
||||
RefreshToken: response.Data.RefreshToken,
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"code": 0,
|
||||
"message": "0",
|
||||
"ttl": 1,
|
||||
"data": {
|
||||
"name":"bilibili",
|
||||
"face":"http://i0.hdslb.com/bfs/face/e1c99895a9f9df4f260a70dc7e227bcb46cf319c.jpg",
|
||||
"openid":"9205eeaa1879skxys969ed47874f225c3"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
type BilibiliUserInfo struct {
|
||||
Name string `json:"name"`
|
||||
Face string `json:"face"`
|
||||
OpenId string `json:"openid`
|
||||
}
|
||||
|
||||
type BilibiliUserInfoResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
TTL int `json:"ttl"`
|
||||
Data BilibiliUserInfo `json:"data"`
|
||||
}
|
||||
|
||||
// GetUserInfo Use access_token to get UserInfo
|
||||
// get more detail via: https://openhome.bilibili.com/doc/4/feb66f99-7d87-c206-00e7-d84164cd701c
|
||||
func (idp *BilibiliIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
accessToken := token.AccessToken
|
||||
clientId := idp.Config.ClientID
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("client_id", clientId)
|
||||
params.Add("access_token", accessToken)
|
||||
|
||||
userInfoUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.AuthURL, params.Encode())
|
||||
|
||||
resp, err := idp.Client.Get(userInfoUrl)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bUserInfoResponse := &BilibiliUserInfoResponse{}
|
||||
if err = json.Unmarshal(data, bUserInfoResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bUserInfoResponse.Code != 0 {
|
||||
return nil, fmt.Errorf("userinfo.Errcode = %d, userinfo.Errmsg = %s", bUserInfoResponse.Code, bUserInfoResponse.Message)
|
||||
}
|
||||
|
||||
userInfo := &UserInfo{
|
||||
Id: bUserInfoResponse.Data.OpenId,
|
||||
Username: bUserInfoResponse.Data.Name,
|
||||
DisplayName: bUserInfoResponse.Data.Name,
|
||||
AvatarUrl: bUserInfoResponse.Data.Face,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (idp *BilibiliIdProvider) postWithBody(body interface{}, url string) ([]byte, error) {
|
||||
bs, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := strings.NewReader(string(bs))
|
||||
resp, err := idp.Client.Post(url, "application/json;charset=UTF-8", r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
return data, nil
|
||||
}
|
@ -131,6 +131,7 @@ func (idp *CasdoorIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
109
idp/custom.go
Normal file
109
idp/custom.go
Normal file
@ -0,0 +1,109 @@
|
||||
// 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 idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
_ "net/url"
|
||||
_ "time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type CustomIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
UserInfoUrl string
|
||||
}
|
||||
|
||||
func NewCustomIdProvider(clientId string, clientSecret string, redirectUrl string, authUrl string, tokenUrl string, userInfoUrl string) *CustomIdProvider {
|
||||
idp := &CustomIdProvider{}
|
||||
idp.UserInfoUrl = userInfoUrl
|
||||
|
||||
var config = &oauth2.Config{
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectUrl,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authUrl,
|
||||
TokenURL: tokenUrl,
|
||||
},
|
||||
}
|
||||
idp.Config = config
|
||||
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *CustomIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
func (idp *CustomIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, idp.Client)
|
||||
return idp.Config.Exchange(ctx, code)
|
||||
}
|
||||
|
||||
type CustomUserInfo struct {
|
||||
Id string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"picture"`
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func (idp *CustomIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
ctUserinfo := &CustomUserInfo{}
|
||||
accessToken := token.AccessToken
|
||||
request, err := http.NewRequest("GET", idp.UserInfoUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//add accessToken to request header
|
||||
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
resp, err := idp.Client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, ctUserinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ctUserinfo.Status != "" {
|
||||
return nil, fmt.Errorf("err: %s", ctUserinfo.Msg)
|
||||
}
|
||||
|
||||
userInfo := &UserInfo{
|
||||
Id: ctUserinfo.Id,
|
||||
Username: ctUserinfo.Name,
|
||||
DisplayName: ctUserinfo.DisplayName,
|
||||
Email: ctUserinfo.Email,
|
||||
AvatarUrl: ctUserinfo.AvatarUrl,
|
||||
}
|
||||
return userInfo, nil
|
||||
}
|
@ -143,6 +143,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
198
idp/douyin.go
Normal file
198
idp/douyin.go
Normal file
@ -0,0 +1,198 @@
|
||||
// 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 idp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type DouyinIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
}
|
||||
|
||||
func NewDouyinIdProvider(clientId string, clientSecret string, redirectUrl string) *DouyinIdProvider {
|
||||
idp := &DouyinIdProvider{}
|
||||
idp.Config = idp.getConfig(clientId, clientSecret, redirectUrl)
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *DouyinIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
func (idp *DouyinIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
|
||||
var endpoint = oauth2.Endpoint{
|
||||
TokenURL: "https://open.douyin.com/oauth/access_token",
|
||||
AuthURL: "https://open.douyin.com/platform/oauth/connect",
|
||||
}
|
||||
|
||||
var config = &oauth2.Config{
|
||||
Scopes: []string{"user_info"},
|
||||
Endpoint: endpoint,
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectUrl,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-permission/get-access-token
|
||||
/*
|
||||
{
|
||||
"data": {
|
||||
"access_token": "access_token",
|
||||
"description": "",
|
||||
"error_code": "0",
|
||||
"expires_in": "86400",
|
||||
"open_id": "aaa-bbb-ccc",
|
||||
"refresh_expires_in": "86400",
|
||||
"refresh_token": "refresh_token",
|
||||
"scope": "user_info"
|
||||
},
|
||||
"message": "<nil>"
|
||||
}
|
||||
*/
|
||||
|
||||
type DouyinTokenResp struct {
|
||||
Data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
OpenId string `json:"open_id"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
} `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GetToken use code to get access_token
|
||||
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-permission/get-access-token
|
||||
func (idp *DouyinIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
payload := url.Values{}
|
||||
payload.Set("code", code)
|
||||
payload.Set("grant_type", "authorization_code")
|
||||
payload.Set("client_key", idp.Config.ClientID)
|
||||
payload.Set("client_secret", idp.Config.ClientSecret)
|
||||
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokenResp := &DouyinTokenResp{}
|
||||
err = json.Unmarshal(data, tokenResp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: tokenResp.Data.AccessToken,
|
||||
RefreshToken: tokenResp.Data.RefreshToken,
|
||||
Expiry: time.Unix(time.Now().Unix()+tokenResp.Data.ExpiresIn, 0),
|
||||
}
|
||||
|
||||
raw := make(map[string]interface{})
|
||||
raw["open_id"] = tokenResp.Data.OpenId
|
||||
token = token.WithExtra(raw)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// get more details via: https://open.douyin.com/platform/doc?doc=docs/openapi/account-management/get-account-open-info
|
||||
/*
|
||||
{
|
||||
"data": {
|
||||
"avatar": "https://example.com/x.jpeg",
|
||||
"city": "上海",
|
||||
"country": "中国",
|
||||
"description": "",
|
||||
"e_account_role": "<nil>",
|
||||
"error_code": "0",
|
||||
"gender": "<nil>",
|
||||
"nickname": "张伟",
|
||||
"open_id": "0da22181-d833-447f-995f-1beefea5bef3",
|
||||
"province": "上海",
|
||||
"union_id": "1ad4e099-4a0c-47d1-a410-bffb4f2f64a4"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
type DouyinUserInfo struct {
|
||||
Data struct {
|
||||
Avatar string `json:"avatar"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
// 0->unknown, 1->male, 2->female
|
||||
Gender int64 `json:"gender"`
|
||||
Nickname string `json:"nickname"`
|
||||
OpenId string `json:"open_id"`
|
||||
Province string `json:"province"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GetUserInfo use token to get user profile
|
||||
func (idp *DouyinIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
body := &struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
OpenId string `json:"open_id"`
|
||||
}{token.AccessToken, token.Extra("open_id").(string)}
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("GET", "https://open.douyin.com/oauth/userinfo/", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("access-token", token.AccessToken)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err := idp.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var douyinUserInfo DouyinUserInfo
|
||||
err = json.Unmarshal(respBody, &douyinUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo := UserInfo{
|
||||
Id: douyinUserInfo.Data.OpenId,
|
||||
Username: douyinUserInfo.Data.Nickname,
|
||||
DisplayName: douyinUserInfo.Data.Nickname,
|
||||
AvatarUrl: douyinUserInfo.Data.Avatar,
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
@ -169,8 +169,11 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
|
||||
|
||||
resp, err := idp.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err = ioutil.ReadAll(resp.Body)
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
200
idp/okta.go
Normal file
200
idp/okta.go
Normal file
@ -0,0 +1,200 @@
|
||||
// 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 idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OktaIdProvider struct {
|
||||
Client *http.Client
|
||||
Config *oauth2.Config
|
||||
Host string
|
||||
}
|
||||
|
||||
func NewOktaIdProvider(clientId string, clientSecret string, redirectUrl string, hostUrl string) *OktaIdProvider {
|
||||
idp := &OktaIdProvider{}
|
||||
|
||||
config := idp.getConfig(hostUrl, clientId, clientSecret, redirectUrl)
|
||||
config.ClientID = clientId
|
||||
config.ClientSecret = clientSecret
|
||||
config.RedirectURL = redirectUrl
|
||||
idp.Config = config
|
||||
idp.Host = hostUrl
|
||||
return idp
|
||||
}
|
||||
|
||||
func (idp *OktaIdProvider) SetHttpClient(client *http.Client) {
|
||||
idp.Client = client
|
||||
}
|
||||
|
||||
func (idp *OktaIdProvider) getConfig(hostUrl string, clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
|
||||
var endpoint = oauth2.Endpoint{
|
||||
TokenURL: fmt.Sprintf("%s/v1/token", hostUrl),
|
||||
AuthURL: fmt.Sprintf("%s/v1/authorize", hostUrl),
|
||||
}
|
||||
|
||||
var config = &oauth2.Config{
|
||||
// openid is required for authentication requests
|
||||
// get more details via: https://developer.okta.com/docs/reference/api/oidc/#reserved-scopes
|
||||
Scopes: []string{"openid", "profile", "email"},
|
||||
Endpoint: endpoint,
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectUrl,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// get more details via: https://developer.okta.com/docs/reference/api/oidc/#token
|
||||
/*
|
||||
{
|
||||
"access_token" : "eyJhbGciOiJSUzI1NiJ9.eyJ2ZXIiOjEsImlzcyI6Imh0dHA6Ly9yYWluLm9rdGExLmNvbToxODAyIiwiaWF0IjoxNDQ5Nj
|
||||
I0MDI2LCJleHAiOjE0NDk2Mjc2MjYsImp0aSI6IlVmU0lURzZCVVNfdHA3N21BTjJxIiwic2NvcGVzIjpbIm9wZW5pZCIsI
|
||||
mVtYWlsIl0sImNsaWVudF9pZCI6InVBYXVub2ZXa2FESnh1a0NGZUJ4IiwidXNlcl9pZCI6IjAwdWlkNEJ4WHc2STZUVjRt
|
||||
MGczIn0.HaBu5oQxdVCIvea88HPgr2O5evqZlCT4UXH4UKhJnZ5px-ArNRqwhxXWhHJisslswjPpMkx1IgrudQIjzGYbtLF
|
||||
jrrg2ueiU5-YfmKuJuD6O2yPWGTsV7X6i7ABT6P-t8PRz_RNbk-U1GXWIEkNnEWbPqYDAm_Ofh7iW0Y8WDA5ez1jbtMvd-o
|
||||
XMvJLctRiACrTMLJQ2e5HkbUFxgXQ_rFPNHJbNSUBDLqdi2rg_ND64DLRlXRY7hupNsvWGo0gF4WEUk8IZeaLjKw8UoIs-E
|
||||
TEwJlAMcvkhoVVOsN5dPAaEKvbyvPC1hUGXb4uuThlwdD3ECJrtwgKqLqcWonNtiw",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600,
|
||||
"scope" : "openid email",
|
||||
"refresh_token" : "a9VpZDRCeFh3Nkk2VdY",
|
||||
"id_token" : "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMHVpZDRCeFh3Nkk2VFY0bTBnMyIsImVtYWlsIjoid2VibWFzdGVyQGNsb3VkaXR1ZG
|
||||
UubmV0IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInZlciI6MSwiaXNzIjoiaHR0cDovL3JhaW4ub2t0YTEuY29tOjE4MDIiLCJsb
|
||||
2dpbiI6ImFkbWluaXN0cmF0b3IxQGNsb3VkaXR1ZGUubmV0IiwiYXVkIjoidUFhdW5vZldrYURKeHVrQ0ZlQngiLCJpYXQiOjE0
|
||||
NDk2MjQwMjYsImV4cCI6MTQ0OTYyNzYyNiwiYW1yIjpbInB3ZCJdLCJqdGkiOiI0ZUFXSk9DTUIzU1g4WGV3RGZWUiIsImF1dGh
|
||||
fdGltZSI6MTQ0OTYyNDAyNiwiYXRfaGFzaCI6ImNwcUtmZFFBNWVIODkxRmY1b0pyX1EifQ.Btw6bUbZhRa89DsBb8KmL9rfhku
|
||||
--_mbNC2pgC8yu8obJnwO12nFBepui9KzbpJhGM91PqJwi_AylE6rp-ehamfnUAO4JL14PkemF45Pn3u_6KKwxJnxcWxLvMuuis
|
||||
nvIs7NScKpOAab6ayZU0VL8W6XAijQmnYTtMWQfSuaaR8rYOaWHrffh3OypvDdrQuYacbkT0csxdrayXfBG3UF5-ZAlhfch1fhF
|
||||
T3yZFdWwzkSDc0BGygfiFyNhCezfyT454wbciSZgrA9ROeHkfPCaX7KCFO8GgQEkGRoQntFBNjluFhNLJIUkEFovEDlfuB4tv_M
|
||||
8BM75celdy3jkpOurg"
|
||||
}
|
||||
*/
|
||||
|
||||
type OktaToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IdToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
// GetToken use code to get access_token
|
||||
// get more details via: https://developer.okta.com/docs/reference/api/oidc/#token
|
||||
func (idp *OktaIdProvider) GetToken(code string) (*oauth2.Token, error) {
|
||||
payload := url.Values{}
|
||||
payload.Set("code", code)
|
||||
payload.Set("grant_type", "authorization_code")
|
||||
payload.Set("client_id", idp.Config.ClientID)
|
||||
payload.Set("client_secret", idp.Config.ClientSecret)
|
||||
payload.Set("redirect_uri", idp.Config.RedirectURL)
|
||||
resp, err := idp.Client.PostForm(idp.Config.Endpoint.TokenURL, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pToken := &OktaToken{}
|
||||
err = json.Unmarshal(data, pToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
|
||||
}
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: pToken.AccessToken,
|
||||
TokenType: "Bearer",
|
||||
RefreshToken: pToken.RefreshToken,
|
||||
Expiry: time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// get more details via: https://developer.okta.com/docs/reference/api/oidc/#userinfo
|
||||
/*
|
||||
{
|
||||
"sub": "00uid4BxXw6I6TV4m0g3",
|
||||
"name" :"John Doe",
|
||||
"nickname":"Jimmy",
|
||||
"given_name":"John",
|
||||
"middle_name":"James",
|
||||
"family_name":"Doe",
|
||||
"profile":"https://example.com/john.doe",
|
||||
"zoneinfo":"America/Los_Angeles",
|
||||
"locale":"en-US",
|
||||
"updated_at":1311280970,
|
||||
"email":"john.doe@example.com",
|
||||
"email_verified":true,
|
||||
"address" : { "street_address":"123 Hollywood Blvd.", "locality":"Los Angeles", "region":"CA", "postal_code":"90210", "country":"US" },
|
||||
"phone_number":"+1 (425) 555-1212"
|
||||
}
|
||||
*/
|
||||
|
||||
type OktaUserInfo struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Picture string `json:"picture"`
|
||||
Sub string `json:"sub"`
|
||||
}
|
||||
|
||||
// GetUserInfo use token to get user profile
|
||||
// get more details via: https://developer.okta.com/docs/reference/api/oidc/#userinfo
|
||||
func (idp *OktaIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/userinfo", idp.Host), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
req.Header.Add("Accept", "application/json")
|
||||
resp, err := idp.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var oktaUserInfo OktaUserInfo
|
||||
err = json.Unmarshal(body, &oktaUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo := UserInfo{
|
||||
Id: oktaUserInfo.Sub,
|
||||
Username: oktaUserInfo.PreferredUsername,
|
||||
DisplayName: oktaUserInfo.Name,
|
||||
Email: oktaUserInfo.Email,
|
||||
AvatarUrl: oktaUserInfo.Picture,
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
@ -35,7 +35,7 @@ type IdProvider interface {
|
||||
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
|
||||
}
|
||||
|
||||
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string) IdProvider {
|
||||
func GetIdProvider(typ string, subType string, clientId string, clientSecret string, appId string, redirectUrl string, hostUrl string, authUrl string, tokenUrl string, userInfoUrl string) IdProvider {
|
||||
if typ == "GitHub" {
|
||||
return NewGithubIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Google" {
|
||||
@ -72,6 +72,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
|
||||
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Alipay" {
|
||||
return NewAlipayIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Custom" {
|
||||
return NewCustomIdProvider(clientId, clientSecret, redirectUrl, authUrl, tokenUrl, userInfoUrl)
|
||||
} else if typ == "Infoflow" {
|
||||
if subType == "Internal" {
|
||||
return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl)
|
||||
@ -82,8 +84,14 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
|
||||
}
|
||||
} else if typ == "Casdoor" {
|
||||
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
||||
} else if typ == "Okta" {
|
||||
return NewOktaIdProvider(clientId, clientSecret, redirectUrl, hostUrl)
|
||||
} else if typ == "Douyin" {
|
||||
return NewDouyinIdProvider(clientId, clientSecret, redirectUrl)
|
||||
} else if isGothSupport(typ) {
|
||||
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
|
||||
} else if typ == "Bilibili" {
|
||||
return NewBilibiliIdProvider(clientId, clientSecret, redirectUrl)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
5
main.go
5
main.go
@ -27,10 +27,11 @@ import (
|
||||
"github.com/casdoor/casdoor/proxy"
|
||||
"github.com/casdoor/casdoor/routers"
|
||||
_ "github.com/casdoor/casdoor/routers"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
createDatabase := flag.Bool("createDatabase", false, "true if you need casdoor to create database")
|
||||
createDatabase := flag.Bool("createDatabase", false, "true if you need Casdoor to create database")
|
||||
flag.Parse()
|
||||
|
||||
object.InitAdapter(*createDatabase)
|
||||
@ -40,7 +41,7 @@ func main() {
|
||||
proxy.InitHttpClient()
|
||||
authz.InitAuthz()
|
||||
|
||||
go object.RunSyncUsersJob()
|
||||
util.SafeGoroutine(func() {object.RunSyncUsersJob()})
|
||||
|
||||
//beego.DelStaticPath("/static")
|
||||
beego.SetStaticPath("/static", "web/build/static")
|
||||
|
@ -138,6 +138,11 @@ func (a *Adapter) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Model))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Provider))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -257,7 +257,11 @@ func UpdateApplication(id string, application *Application) bool {
|
||||
providerItem.Provider = nil
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(application)
|
||||
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
if application.ClientSecret == "***" {
|
||||
session.Omit("client_secret")
|
||||
}
|
||||
affected, err := session.Update(application)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/cred"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
@ -195,3 +196,37 @@ func CheckUserPassword(organization string, username string, password string) (*
|
||||
func filterField(field string) bool {
|
||||
return reFieldWhiteList.MatchString(field)
|
||||
}
|
||||
|
||||
func CheckUserPermission(requestUserId, userId string, strict bool) (bool, error) {
|
||||
if requestUserId == "" {
|
||||
return false, fmt.Errorf("please login first")
|
||||
}
|
||||
|
||||
targetUser := GetUser(userId)
|
||||
if targetUser == nil {
|
||||
return false, fmt.Errorf("the user: %s doesn't exist", userId)
|
||||
}
|
||||
|
||||
hasPermission := false
|
||||
if strings.HasPrefix(requestUserId, "app/") {
|
||||
hasPermission = true
|
||||
} else {
|
||||
requestUser := GetUser(requestUserId)
|
||||
if requestUser == nil {
|
||||
return false, fmt.Errorf("session outdated, please login again")
|
||||
}
|
||||
if requestUser.IsGlobalAdmin {
|
||||
hasPermission = true
|
||||
} else if requestUserId == userId {
|
||||
hasPermission = true
|
||||
} else if targetUser.Owner == requestUser.Owner {
|
||||
if strict {
|
||||
hasPermission = requestUser.IsAdmin
|
||||
} else {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasPermission, fmt.Errorf("you don't have the permission to do this")
|
||||
}
|
@ -109,7 +109,7 @@ func initBuiltInApplication() {
|
||||
{Name: "Display name", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Password", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Confirm password", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Email", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Email", Visible: true, Required: true, Prompted: false, Rule: "Normal"},
|
||||
{Name: "Phone", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
{Name: "Agreement", Visible: true, Required: true, Prompted: false, Rule: "None"},
|
||||
},
|
||||
@ -147,7 +147,7 @@ func initBuiltInCert() {
|
||||
DisplayName: "Built-in Cert",
|
||||
Scope: "JWT",
|
||||
Type: "x509",
|
||||
CryptoAlgorithm: "RSA",
|
||||
CryptoAlgorithm: "RS256",
|
||||
BitSize: 4096,
|
||||
ExpireInYears: 20,
|
||||
PublicKey: tokenJwtPublicKey,
|
||||
|
105
object/ldap.go
105
object/ldap.go
@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/thanhpk/randstr"
|
||||
@ -42,6 +43,7 @@ type Ldap struct {
|
||||
|
||||
type ldapConn struct {
|
||||
Conn *goldap.Conn
|
||||
IsAD bool
|
||||
}
|
||||
|
||||
//type ldapGroup struct {
|
||||
@ -78,6 +80,13 @@ type LdapRespUser struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type ldapServerType struct {
|
||||
Vendorname string
|
||||
Vendorversion string
|
||||
IsGlobalCatalogReady string
|
||||
ForestFunctionality string
|
||||
}
|
||||
|
||||
func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
|
||||
returnAnyNotEmpty := func(strs ...string) string {
|
||||
for _, str := range strs {
|
||||
@ -104,6 +113,45 @@ func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
|
||||
return res
|
||||
}
|
||||
|
||||
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
|
||||
SearchFilter := "(objectclass=*)"
|
||||
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
|
||||
|
||||
searchReq := goldap.NewSearchRequest("",
|
||||
goldap.ScopeBaseObject, goldap.NeverDerefAliases, 0, 0, false,
|
||||
SearchFilter, SearchAttributes, nil)
|
||||
searchResult, err := Conn.Search(searchReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(searchResult.Entries) == 0 {
|
||||
return false, errors.New("no result")
|
||||
}
|
||||
isMicrosoft := false
|
||||
var ldapServerType ldapServerType
|
||||
for _, entry := range searchResult.Entries {
|
||||
for _, attribute := range entry.Attributes {
|
||||
switch attribute.Name {
|
||||
case "vendorname":
|
||||
ldapServerType.Vendorname = attribute.Values[0]
|
||||
case "vendorversion":
|
||||
ldapServerType.Vendorversion = attribute.Values[0]
|
||||
case "isGlobalCatalogReady":
|
||||
ldapServerType.IsGlobalCatalogReady = attribute.Values[0]
|
||||
case "forestFunctionality":
|
||||
ldapServerType.ForestFunctionality = attribute.Values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if ldapServerType.Vendorname == "" &&
|
||||
ldapServerType.Vendorversion == "" &&
|
||||
ldapServerType.IsGlobalCatalogReady == "TRUE" &&
|
||||
ldapServerType.ForestFunctionality != "" {
|
||||
isMicrosoft = true
|
||||
}
|
||||
return isMicrosoft, err
|
||||
}
|
||||
|
||||
func GetLdapConn(host string, port int, adminUser string, adminPasswd string) (*ldapConn, error) {
|
||||
conn, err := goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
@ -115,7 +163,11 @@ func GetLdapConn(host string, port int, adminUser string, adminPasswd string) (*
|
||||
return nil, fmt.Errorf("fail to login Ldap server with [%s]", adminUser)
|
||||
}
|
||||
|
||||
return &ldapConn{Conn: conn}, nil
|
||||
isAD, err := isMicrosoftAD(conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to get Ldap server type [%s]", adminUser)
|
||||
}
|
||||
return &ldapConn{Conn: conn, IsAD: isAD}, nil
|
||||
}
|
||||
|
||||
//FIXME: The Base DN does not necessarily contain the Group
|
||||
@ -158,10 +210,19 @@ func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
|
||||
SearchFilter := "(objectClass=posixAccount)"
|
||||
SearchAttributes := []string{"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email",
|
||||
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"}
|
||||
|
||||
searchReq := goldap.NewSearchRequest(baseDn,
|
||||
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
|
||||
SearchFilter, SearchAttributes, nil)
|
||||
SearchFilterMsAD := "(objectClass=user)"
|
||||
SearchAttributesMsAD := []string{"uidNumber", "sAMAccountName", "cn", "gidNumber", "entryUUID", "mail", "email",
|
||||
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"}
|
||||
var searchReq *goldap.SearchRequest
|
||||
if l.IsAD {
|
||||
searchReq = goldap.NewSearchRequest(baseDn,
|
||||
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
|
||||
SearchFilterMsAD, SearchAttributesMsAD, nil)
|
||||
} else {
|
||||
searchReq = goldap.NewSearchRequest(baseDn,
|
||||
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
|
||||
SearchFilter, SearchAttributes, nil)
|
||||
}
|
||||
searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -181,12 +242,16 @@ func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
|
||||
ldapUserItem.UidNumber = attribute.Values[0]
|
||||
case "uid":
|
||||
ldapUserItem.Uid = attribute.Values[0]
|
||||
case "sAMAccountName":
|
||||
ldapUserItem.Uid = attribute.Values[0]
|
||||
case "cn":
|
||||
ldapUserItem.Cn = attribute.Values[0]
|
||||
case "gidNumber":
|
||||
ldapUserItem.GidNumber = attribute.Values[0]
|
||||
case "entryUUID":
|
||||
ldapUserItem.Uuid = attribute.Values[0]
|
||||
case "objectGUID":
|
||||
ldapUserItem.Uuid = attribute.Values[0]
|
||||
case "mail":
|
||||
ldapUserItem.Mail = attribute.Values[0]
|
||||
case "email":
|
||||
@ -300,7 +365,7 @@ func DeleteLdap(ldap *Ldap) bool {
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]LdapRespUser) {
|
||||
func SyncLdapUsers(owner string, users []LdapRespUser, ldapId string) (*[]LdapRespUser, *[]LdapRespUser) {
|
||||
var existUsers []LdapRespUser
|
||||
var failedUsers []LdapRespUser
|
||||
var uuids []string
|
||||
@ -311,6 +376,25 @@ func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]Ldap
|
||||
|
||||
existUuids := CheckLdapUuidExist(owner, uuids)
|
||||
|
||||
organization := getOrganization("admin", owner)
|
||||
ldap := GetLdap(ldapId)
|
||||
|
||||
var dc []string
|
||||
for _, basedn := range strings.Split(ldap.BaseDn, ",") {
|
||||
if strings.Contains(basedn, "dc=") {
|
||||
dc = append(dc, basedn[3:])
|
||||
}
|
||||
}
|
||||
affiliation := strings.Join(dc, ".")
|
||||
|
||||
var ou []string
|
||||
for _, admin := range strings.Split(ldap.Admin, ",") {
|
||||
if strings.Contains(admin, "ou=") {
|
||||
ou = append(ou, admin[3:])
|
||||
}
|
||||
}
|
||||
tag := strings.Join(ou, ".")
|
||||
|
||||
for _, user := range users {
|
||||
found := false
|
||||
if len(existUuids) > 0 {
|
||||
@ -325,15 +409,14 @@ func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]Ldap
|
||||
Owner: owner,
|
||||
Name: buildLdapUserName(user.Uid, user.UidNumber),
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
Password: "123",
|
||||
DisplayName: user.Cn,
|
||||
Avatar: "https://casbin.org/img/casbin.svg",
|
||||
Avatar: organization.DefaultAvatar,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Address: []string{user.Address},
|
||||
Affiliation: "Example Inc.",
|
||||
Tag: "staff",
|
||||
Score: 2000,
|
||||
Affiliation: affiliation,
|
||||
Tag: tag,
|
||||
Score: beego.AppConfig.DefaultInt("initScore", 2000),
|
||||
Ldap: user.Uuid,
|
||||
}) {
|
||||
failedUsers = append(failedUsers, user)
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/logs"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
type LdapAutoSynchronizer struct {
|
||||
@ -47,7 +48,7 @@ func (l *LdapAutoSynchronizer) StartAutoSync(ldapId string) error {
|
||||
stopChan := make(chan struct{})
|
||||
l.ldapIdToStopChan[ldapId] = stopChan
|
||||
logs.Info(fmt.Sprintf("autoSync started for %s", ldap.Id))
|
||||
go l.syncRoutine(ldap, stopChan)
|
||||
util.SafeGoroutine(func() {l.syncRoutine(ldap, stopChan)})
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -85,7 +86,7 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) {
|
||||
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
|
||||
continue
|
||||
}
|
||||
existed, failed := SyncLdapUsers(ldap.Owner, LdapUsersToLdapRespUsers(users))
|
||||
existed, failed := SyncLdapUsers(ldap.Owner, LdapUsersToLdapRespUsers(users), ldap.Id)
|
||||
if len(*failed) != 0 {
|
||||
logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(*existed)-len(*failed), len(*failed)), *failed)
|
||||
} else {
|
||||
|
122
object/model.go
Normal file
122
object/model.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
type Model 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"`
|
||||
|
||||
ModelText string `xorm:"mediumtext" json:"modelText"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
}
|
||||
|
||||
func GetModelCount(owner, field, value string) int {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Model{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func GetModels(owner string) []*Model {
|
||||
models := []*Model{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&models, &Model{Owner: owner})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func GetPaginationModels(owner string, offset, limit int, field, value, sortField, sortOrder string) []*Model {
|
||||
models := []*Model{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&models)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func getModel(owner string, name string) *Model {
|
||||
if owner == "" || name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
model := Model{Owner: owner, Name: name}
|
||||
existed, err := adapter.Engine.Get(&model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &model
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetModel(id string) *Model {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getModel(owner, name)
|
||||
}
|
||||
|
||||
func UpdateModel(id string, model *Model) bool {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
if getModel(owner, name) == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func AddModel(model *Model) bool {
|
||||
affected, err := adapter.Engine.Insert(model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func DeleteModel(model *Model) bool {
|
||||
affected, err := adapter.Engine.ID(core.PK{model.Owner, model.Name}).Delete(&Model{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func (model *Model) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", model.Owner, model.Name)
|
||||
}
|
@ -76,7 +76,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
|
||||
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
|
||||
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
|
||||
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
|
||||
ResponseTypesSupported: []string{"id_token"},
|
||||
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
|
||||
ResponseModesSupported: []string{"login", "code", "link"},
|
||||
GrantTypesSupported: []string{"password", "authorization_code"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
@ -105,6 +105,8 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
|
||||
jwk.Key = x509Cert.PublicKey
|
||||
jwk.Certificates = []*x509.Certificate{x509Cert}
|
||||
jwk.KeyID = cert.Name
|
||||
jwk.Algorithm = cert.CryptoAlgorithm
|
||||
jwk.Use = "sig"
|
||||
jwks.Keys = append(jwks.Keys, jwk)
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ type Organization struct {
|
||||
Tags []string `xorm:"mediumtext" json:"tags"`
|
||||
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
|
||||
EnableSoftDeletion bool `json:"enableSoftDeletion"`
|
||||
IsProfilePublic bool `json:"isProfilePublic"`
|
||||
}
|
||||
|
||||
func GetOrganizationCount(owner, field, value string) int {
|
||||
@ -127,7 +128,7 @@ func UpdateOrganization(id string, organization *Organization) bool {
|
||||
}
|
||||
}
|
||||
|
||||
if organization.MasterPassword != "" {
|
||||
if organization.MasterPassword != "" && organization.MasterPassword != "***" {
|
||||
credManager := cred.GetCredManager(organization.PasswordType)
|
||||
if credManager != nil {
|
||||
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, "", organization.PasswordSalt)
|
||||
@ -135,7 +136,11 @@ func UpdateOrganization(id string, organization *Organization) bool {
|
||||
}
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(organization)
|
||||
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
if organization.MasterPassword == "***" {
|
||||
session.Omit("master_password")
|
||||
}
|
||||
affected, err := session.Update(organization)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -44,6 +44,16 @@ type Payment struct {
|
||||
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
|
||||
State string `xorm:"varchar(100)" json:"state"`
|
||||
Message string `xorm:"varchar(1000)" json:"message"`
|
||||
|
||||
PersonName string `xorm:"varchar(100)" json:"personName"`
|
||||
PersonIdCard string `xorm:"varchar(100)" json:"personIdCard"`
|
||||
PersonEmail string `xorm:"varchar(100)" json:"personEmail"`
|
||||
PersonPhone string `xorm:"varchar(100)" json:"personPhone"`
|
||||
InvoiceType string `xorm:"varchar(100)" json:"invoiceType"`
|
||||
InvoiceTitle string `xorm:"varchar(100)" json:"invoiceTitle"`
|
||||
InvoiceTaxId string `xorm:"varchar(100)" json:"invoiceTaxId"`
|
||||
InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"`
|
||||
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
|
||||
}
|
||||
|
||||
func GetPaymentCount(owner, field, value string) int {
|
||||
@ -197,6 +207,44 @@ func NotifyPayment(request *http.Request, body []byte, owner string, providerNam
|
||||
return ok
|
||||
}
|
||||
|
||||
func invoicePayment(payment *Payment) (string, error) {
|
||||
provider := getProvider(payment.Owner, payment.Provider)
|
||||
if provider == nil {
|
||||
return "", fmt.Errorf("the payment provider: %s does not exist", payment.Provider)
|
||||
}
|
||||
|
||||
pProvider, _, err := provider.getPaymentProvider()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
invoiceUrl, err := pProvider.GetInvoice(payment.Name, payment.PersonName, payment.PersonIdCard, payment.PersonEmail, payment.PersonPhone, payment.InvoiceType, payment.InvoiceTitle, payment.InvoiceTaxId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return invoiceUrl, nil
|
||||
}
|
||||
|
||||
func InvoicePayment(payment *Payment) (string, error) {
|
||||
if payment.State != "Paid" {
|
||||
return "", fmt.Errorf("the payment state is supposed to be: \"%s\", got: \"%s\"", "Paid", payment.State)
|
||||
}
|
||||
|
||||
invoiceUrl, err := invoicePayment(payment)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payment.InvoiceUrl = invoiceUrl
|
||||
affected := UpdatePayment(payment.GetId(), payment)
|
||||
if !affected {
|
||||
return "", fmt.Errorf("failed to update the payment: %s", payment.Name)
|
||||
}
|
||||
|
||||
return invoiceUrl, nil
|
||||
}
|
||||
|
||||
func (payment *Payment) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", payment.Owner, payment.Name)
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ type Permission struct {
|
||||
Users []string `xorm:"mediumtext" json:"users"`
|
||||
Roles []string `xorm:"mediumtext" json:"roles"`
|
||||
|
||||
Model string `xorm:"varchar(100)" json:"model"`
|
||||
ResourceType string `xorm:"varchar(100)" json:"resourceType"`
|
||||
Resources []string `xorm:"mediumtext" json:"resources"`
|
||||
Actions []string `xorm:"mediumtext" json:"actions"`
|
||||
|
@ -170,6 +170,7 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
|
||||
|
||||
owner := product.Owner
|
||||
productName := product.Name
|
||||
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
||||
paymentName := util.GenerateTimeId()
|
||||
productDisplayName := product.DisplayName
|
||||
|
||||
@ -177,7 +178,7 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
|
||||
returnUrl := fmt.Sprintf("%s/payments/%s/result", originFrontend, paymentName)
|
||||
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s/%s/%s", originBackend, owner, providerName, productName, paymentName)
|
||||
|
||||
payUrl, err := pProvider.Pay(providerName, productName, paymentName, productDisplayName, product.Price, returnUrl, notifyUrl)
|
||||
payUrl, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, returnUrl, notifyUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -27,16 +27,21 @@ type Provider struct {
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
SubType string `xorm:"varchar(100)" json:"subType"`
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
|
||||
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
|
||||
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Category string `xorm:"varchar(100)" json:"category"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
SubType string `xorm:"varchar(100)" json:"subType"`
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
|
||||
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
|
||||
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
|
||||
Cert string `xorm:"varchar(100)" json:"cert"`
|
||||
CustomAuthUrl string `xorm:"varchar(200)" json:"customAuthUrl"`
|
||||
CustomScope string `xorm:"varchar(200)" json:"customScope"`
|
||||
CustomTokenUrl string `xorm:"varchar(200)" json:"customTokenUrl"`
|
||||
CustomUserInfoUrl string `xorm:"varchar(200)" json:"customUserInfoUrl"`
|
||||
CustomLogo string `xorm:"varchar(200)" json:"customLogo"`
|
||||
|
||||
Host string `xorm:"varchar(100)" json:"host"`
|
||||
Port int `json:"port"`
|
||||
@ -167,7 +172,14 @@ func UpdateProvider(id string, provider *Provider) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(provider)
|
||||
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
if provider.ClientSecret == "***" {
|
||||
session = session.Omit("client_secret")
|
||||
}
|
||||
if provider.ClientSecret2 == "***" {
|
||||
session = session.Omit("client_secret2")
|
||||
}
|
||||
affected, err := session.Update(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
|
||||
type Resource struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(200) notnull pk" json:"name"`
|
||||
Name string `xorm:"varchar(250) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
@ -31,7 +31,7 @@ type Resource struct {
|
||||
Application string `xorm:"varchar(100)" json:"application"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
Parent string `xorm:"varchar(100)" json:"parent"`
|
||||
FileName string `xorm:"varchar(100)" json:"fileName"`
|
||||
FileName string `xorm:"varchar(1000)" json:"fileName"`
|
||||
FileType string `xorm:"varchar(100)" json:"fileType"`
|
||||
FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
|
||||
FileSize int `json:"fileSize"`
|
||||
|
@ -133,7 +133,11 @@ func UpdateSyncer(id string, syncer *Syncer) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(syncer)
|
||||
session := adapter.Engine.ID(core.PK{owner, name}).AllCols()
|
||||
if syncer.Password == "***" {
|
||||
session.Omit("password")
|
||||
}
|
||||
affected, err := session.Update(syncer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -206,3 +210,8 @@ func (syncer *Syncer) getTable() string {
|
||||
return syncer.Table
|
||||
}
|
||||
}
|
||||
|
||||
func RunSyncer(syncer *Syncer) {
|
||||
syncer.initAdapter()
|
||||
syncer.syncUsers()
|
||||
}
|
||||
|
@ -173,7 +173,18 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
|
||||
}
|
||||
|
||||
for _, tableColumn := range syncer.TableColumns {
|
||||
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, result[tableColumn.Name])
|
||||
value := ""
|
||||
if strings.Contains(tableColumn.Name, "+") {
|
||||
names := strings.Split(tableColumn.Name, "+")
|
||||
var values []string
|
||||
for _, name := range names {
|
||||
values = append(values, result[strings.Trim(name, " ")])
|
||||
}
|
||||
value = strings.Join(values, " ")
|
||||
} else {
|
||||
value = result[tableColumn.Name]
|
||||
}
|
||||
syncer.setUserByKeyValue(originalUser, tableColumn.CasdoorName, value)
|
||||
}
|
||||
|
||||
if syncer.Type == "Keycloak" {
|
||||
|
@ -27,6 +27,10 @@ import (
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
const (
|
||||
hourSeconds = 3600
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Message string `xorm:"varchar(100)" json:"message"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
@ -292,7 +296,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeChallenge: challenge,
|
||||
@ -463,7 +467,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: newAccessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
@ -572,7 +576,7 @@ func GetPasswordToken(application *Application, username string, password string
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
@ -604,7 +608,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
|
||||
User: nullUser.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
@ -629,7 +633,7 @@ func GetTokenByUser(application *Application, user *User, scope string, host str
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
ExpiresIn: application.ExpireInHours * hourSeconds,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
CodeIsUsed: true,
|
||||
|
@ -94,6 +94,10 @@ type User struct {
|
||||
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
|
||||
Slack string `xorm:"slack varchar(100)" json:"slack"`
|
||||
Steam string `xorm:"steam varchar(100)" json:"steam"`
|
||||
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
|
||||
Okta string `xorm:"okta varchar(100)" json:"okta"`
|
||||
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
|
||||
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
||||
|
||||
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
||||
Properties map[string]string `json:"properties"`
|
||||
@ -312,6 +316,9 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Password == "***" {
|
||||
user.Password = oldUser.Password
|
||||
}
|
||||
user.UpdateUserHash()
|
||||
|
||||
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {
|
||||
|
@ -97,3 +97,14 @@ func TestGetMaskedUsers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserByField(t *testing.T) {
|
||||
InitConfig()
|
||||
|
||||
user := GetUserByField("built-in", "DingTalk", "test")
|
||||
if user != nil {
|
||||
t.Logf("%+v", user)
|
||||
} else {
|
||||
t.Log("no user found")
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func GetUserByField(organizationName string, field string, value string) *User {
|
||||
}
|
||||
|
||||
user := User{Owner: organizationName}
|
||||
existed, err := adapter.Engine.Where(fmt.Sprintf("%s=?", field), value).Get(&user)
|
||||
existed, err := adapter.Engine.Where(fmt.Sprintf("%s=?", strings.ToLower(field)), value).Get(&user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
|
||||
return pp
|
||||
}
|
||||
|
||||
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
|
||||
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
|
||||
//pp.Client.DebugSwitch = gopay.DebugOn
|
||||
|
||||
bm := gopay.BodyMap{}
|
||||
@ -90,3 +90,7 @@ func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, auth
|
||||
|
||||
return productDisplayName, paymentName, price, productName, providerName, nil
|
||||
}
|
||||
|
||||
func (pp *AlipayPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
112
pp/gc.go
112
pp/gc.go
@ -38,11 +38,14 @@ type GcPayReqInfo struct {
|
||||
OrderDate string `json:"orderdate"`
|
||||
OrderNo string `json:"orderno"`
|
||||
Amount string `json:"amount"`
|
||||
PayerId string `json:"payerid"`
|
||||
PayerName string `json:"payername"`
|
||||
Xmpch string `json:"xmpch"`
|
||||
Body string `json:"body"`
|
||||
ReturnUrl string `json:"return_url"`
|
||||
NotifyUrl string `json:"notify_url"`
|
||||
PayerId string `json:"payerid"`
|
||||
PayerName string `json:"payername"`
|
||||
Remark1 string `json:"remark1"`
|
||||
Remark2 string `json:"remark2"`
|
||||
}
|
||||
|
||||
type GcPayRespInfo struct {
|
||||
@ -87,6 +90,27 @@ type GcResponseBody struct {
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type GcInvoiceReqInfo struct {
|
||||
BusNo string `json:"busno"`
|
||||
PayerName string `json:"payername"`
|
||||
IdNum string `json:"idnum"`
|
||||
PayerType string `json:"payertype"`
|
||||
InvoiceTitle string `json:"invoicetitle"`
|
||||
Tin string `json:"tin"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type GcInvoiceRespInfo struct {
|
||||
BusNo string `json:"busno"`
|
||||
State string `json:"state"`
|
||||
EbillCode string `json:"ebillcode"`
|
||||
EbillNo string `json:"ebillno"`
|
||||
CheckCode string `json:"checkcode"`
|
||||
Url string `json:"url"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func NewGcPaymentProvider(clientId string, clientSecret string, host string) *GcPaymentProvider {
|
||||
pp := &GcPaymentProvider{}
|
||||
|
||||
@ -130,16 +154,17 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
func (pp *GcPaymentProvider) Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
|
||||
func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
|
||||
payReqInfo := GcPayReqInfo{
|
||||
OrderDate: util.GenerateSimpleTimeId(),
|
||||
OrderNo: util.GenerateTimeId(),
|
||||
OrderNo: paymentName,
|
||||
Amount: getPriceString(price),
|
||||
PayerId: "",
|
||||
PayerName: "",
|
||||
Xmpch: pp.Xmpch,
|
||||
Body: productDisplayName,
|
||||
ReturnUrl: returnUrl,
|
||||
NotifyUrl: notifyUrl,
|
||||
Remark1: payerName,
|
||||
Remark2: productName,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(payReqInfo)
|
||||
@ -230,3 +255,78 @@ func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorit
|
||||
|
||||
return productDisplayName, paymentName, price, productName, providerName, nil
|
||||
}
|
||||
|
||||
func (pp *GcPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
payerType := "0"
|
||||
if invoiceType == "Organization" {
|
||||
payerType = "1"
|
||||
}
|
||||
|
||||
invoiceReqInfo := GcInvoiceReqInfo{
|
||||
BusNo: paymentName,
|
||||
PayerName: personName,
|
||||
IdNum: personIdCard,
|
||||
PayerType: payerType,
|
||||
InvoiceTitle: invoiceTitle,
|
||||
Tin: invoiceTaxId,
|
||||
Phone: personPhone,
|
||||
Email: personEmail,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(invoiceReqInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body := GcRequestBody{
|
||||
Op: "InvoiceEBillByOrder",
|
||||
Xmpch: pp.Xmpch,
|
||||
Version: "1.4",
|
||||
Data: base64.StdEncoding.EncodeToString(b),
|
||||
RequestTime: util.GenerateSimpleTimeId(),
|
||||
}
|
||||
|
||||
params := fmt.Sprintf("data=%s&op=%s&requesttime=%s&version=%s&xmpch=%s%s", body.Data, body.Op, body.RequestTime, body.Version, body.Xmpch, pp.SecretKey)
|
||||
body.Sign = strings.ToUpper(util.GetMd5Hash(params))
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
respBytes, err := pp.doPost(bodyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var respBody GcResponseBody
|
||||
err = json.Unmarshal(respBytes, &respBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if respBody.ReturnCode != "SUCCESS" {
|
||||
return "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
|
||||
}
|
||||
|
||||
invoiceRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var invoiceRespInfo GcInvoiceRespInfo
|
||||
err = json.Unmarshal(invoiceRespInfoBytes, &invoiceRespInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if invoiceRespInfo.State == "0" {
|
||||
return "", fmt.Errorf("申请成功,开票中")
|
||||
}
|
||||
|
||||
if invoiceRespInfo.Url == "" {
|
||||
return "", fmt.Errorf("invoice URL is empty")
|
||||
}
|
||||
|
||||
return invoiceRespInfo.Url, nil
|
||||
}
|
||||
|
@ -17,8 +17,9 @@ package pp
|
||||
import "net/http"
|
||||
|
||||
type PaymentProvider interface {
|
||||
Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error)
|
||||
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error)
|
||||
Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error)
|
||||
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
|
||||
}
|
||||
|
||||
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {
|
||||
|
@ -104,6 +104,11 @@ func getUrlPath(urlPath string) string {
|
||||
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
|
||||
return "/cas"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(urlPath, "/api/login/oauth") {
|
||||
return "/api/login/oauth"
|
||||
}
|
||||
|
||||
return urlPath
|
||||
}
|
||||
|
||||
|
@ -65,5 +65,5 @@ func RecordMessage(ctx *context.Context) {
|
||||
record.Organization, record.User = util.GetOwnerAndNameFromId(userId)
|
||||
}
|
||||
|
||||
go object.AddRecord(record)
|
||||
util.SafeGoroutine(func() { object.AddRecord(record) })
|
||||
}
|
||||
|
@ -84,6 +84,12 @@ func initAPI() {
|
||||
beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission")
|
||||
beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission")
|
||||
|
||||
beego.Router("/api/get-models", &controllers.ApiController{}, "GET:GetModels")
|
||||
beego.Router("/api/get-model", &controllers.ApiController{}, "GET:GetModel")
|
||||
beego.Router("/api/update-model", &controllers.ApiController{}, "POST:UpdateModel")
|
||||
beego.Router("/api/add-model", &controllers.ApiController{}, "POST:AddModel")
|
||||
beego.Router("/api/delete-model", &controllers.ApiController{}, "POST:DeleteModel")
|
||||
|
||||
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
|
||||
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
|
||||
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone")
|
||||
@ -145,6 +151,7 @@ func initAPI() {
|
||||
beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer")
|
||||
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
|
||||
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
|
||||
beego.Router("/api/run-syncer", &controllers.ApiController{}, "GET:RunSyncer")
|
||||
|
||||
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
|
||||
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
|
||||
@ -166,6 +173,7 @@ func initAPI() {
|
||||
beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment")
|
||||
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment")
|
||||
beego.Router("/api/notify-payment/?:owner/?:provider/?:product/?:payment", &controllers.ApiController{}, "POST:NotifyPayment")
|
||||
beego.Router("/api/invoice-payment", &controllers.ApiController{}, "POST:InvoicePayment")
|
||||
|
||||
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
|
||||
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
|
||||
|
@ -15,8 +15,8 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/qor/oss"
|
||||
"github.com/qor/oss/aliyun"
|
||||
"github.com/casdoor/oss"
|
||||
"github.com/casdoor/oss/aliyun"
|
||||
)
|
||||
|
||||
func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
|
@ -16,8 +16,8 @@ package storage
|
||||
|
||||
import (
|
||||
awss3 "github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/qor/oss"
|
||||
"github.com/qor/oss/s3"
|
||||
"github.com/casdoor/oss"
|
||||
"github.com/casdoor/oss/s3"
|
||||
)
|
||||
|
||||
func NewAwsS3StorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
|
31
storage/azure.go
Normal file
31
storage/azure.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/casdoor/oss"
|
||||
"github.com/casdoor/oss/azureblob"
|
||||
)
|
||||
|
||||
func NewAzureBlobStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
sp := azureblob.New(&azureblob.Config{
|
||||
AccessId: clientId,
|
||||
AccessKey: clientSecret,
|
||||
Region: region,
|
||||
Bucket: bucket,
|
||||
Endpoint: endpoint,
|
||||
})
|
||||
return sp
|
||||
}
|
@ -20,7 +20,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/qor/oss"
|
||||
"github.com/casdoor/oss"
|
||||
)
|
||||
|
||||
var baseFolder = "files"
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
package storage
|
||||
|
||||
import "github.com/qor/oss"
|
||||
import "github.com/casdoor/oss"
|
||||
|
||||
func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
switch providerType {
|
||||
@ -26,6 +26,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
|
||||
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint)
|
||||
case "Tencent Cloud COS":
|
||||
return NewTencentCloudCosStorageProvider(clientId, clientSecret, region, bucket, endpoint)
|
||||
case "Azure Blob":
|
||||
return NewAzureBlobStorageProvider(clientId, clientSecret, region, bucket, endpoint)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -15,8 +15,8 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/qor/oss"
|
||||
"github.com/qor/oss/tencent"
|
||||
"github.com/casdoor/oss"
|
||||
"github.com/casdoor/oss/tencent"
|
||||
)
|
||||
|
||||
func NewTencentCloudCosStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
|
||||
|
38
util/util.go
Normal file
38
util/util.go
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/astaxie/beego/logs"
|
||||
)
|
||||
|
||||
func SafeGoroutine(fn func()) {
|
||||
var err error
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
err, ok = r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
logs.Error("goroutine panic: %v", err)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
@ -69,6 +69,8 @@ import PromptPage from "./auth/PromptPage";
|
||||
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
|
||||
import SamlCallback from './auth/SamlCallback';
|
||||
import CasLogout from "./auth/CasLogout";
|
||||
import ModelListPage from "./ModelListPage";
|
||||
import ModelEditPage from "./ModelEditPage";
|
||||
|
||||
const { Header, Footer } = Layout;
|
||||
|
||||
@ -118,6 +120,8 @@ class App extends Component {
|
||||
this.setState({ selectedMenuKey: '/roles' });
|
||||
} else if (uri.includes('/permissions')) {
|
||||
this.setState({ selectedMenuKey: '/permissions' });
|
||||
} else if (uri.includes('/models')) {
|
||||
this.setState({ selectedMenuKey: '/models' });
|
||||
} else if (uri.includes('/providers')) {
|
||||
this.setState({ selectedMenuKey: '/providers' });
|
||||
} else if (uri.includes('/applications')) {
|
||||
@ -382,6 +386,13 @@ class App extends Component {
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
res.push(
|
||||
<Menu.Item key="/models">
|
||||
<Link to="/models">
|
||||
{i18next.t("general:Models")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
);
|
||||
res.push(
|
||||
<Menu.Item key="/providers">
|
||||
<Link to="/providers">
|
||||
@ -514,6 +525,8 @@ class App extends Component {
|
||||
<Route exact path="/roles/:organizationName/:roleName" render={(props) => this.renderLoginIfNotLoggedIn(<RoleEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/permissions" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionListPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/permissions/:organizationName/:permissionName" render={(props) => this.renderLoginIfNotLoggedIn(<PermissionEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/models" render={(props) => this.renderLoginIfNotLoggedIn(<ModelListPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/models/:organizationName/:modelName" render={(props) => this.renderLoginIfNotLoggedIn(<ModelEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/providers/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)}/>
|
||||
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>
|
||||
|
@ -35,6 +35,7 @@ require('codemirror/theme/material-darker.css');
|
||||
require("codemirror/mode/htmlmixed/htmlmixed");
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
class ApplicationEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
@ -48,6 +49,7 @@ class ApplicationEditPage extends React.Component {
|
||||
providers: [],
|
||||
uploading: false,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
samlMetadata: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -56,6 +58,7 @@ class ApplicationEditPage extends React.Component {
|
||||
this.getOrganizations();
|
||||
this.getCerts();
|
||||
this.getProviders();
|
||||
this.getSamlMetadata();
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
@ -97,6 +100,15 @@ class ApplicationEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getSamlMetadata() {
|
||||
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
samlMetadata: res,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
parseApplicationField(key, value) {
|
||||
if (["expireInHours", "refreshExpireInHours"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
@ -454,12 +466,21 @@ class ApplicationEditPage extends React.Component {
|
||||
{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>)
|
||||
{id: "id_token", name: "ID Token"},
|
||||
{id: "refresh_token", name: "Refresh 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("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TextArea rows={8} value={this.state.samlMetadata} />
|
||||
</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"))} :
|
||||
|
@ -43,7 +43,7 @@ class ApplicationListPage extends BaseListPage {
|
||||
{name: "Display name", visible: true, required: true, rule: "None"},
|
||||
{name: "Password", visible: true, required: true, rule: "None"},
|
||||
{name: "Confirm password", visible: true, required: true, rule: "None"},
|
||||
{name: "Email", visible: true, required: true, rule: "None"},
|
||||
{name: "Email", visible: true, required: true, rule: "Normal"},
|
||||
{name: "Phone", visible: true, required: true, rule: "None"},
|
||||
{name: "Agreement", visible: true, required: true, rule: "None"},
|
||||
],
|
||||
|
@ -136,7 +136,7 @@ class CertEditPage extends React.Component {
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: 'RSA', name: 'RSA'},
|
||||
{id: 'RS256', name: 'RS256'},
|
||||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
|
@ -32,7 +32,7 @@ class CertListPage extends BaseListPage {
|
||||
displayName: `New Cert - ${randomName}`,
|
||||
scope: "JWT",
|
||||
type: "x509",
|
||||
cryptoAlgorithm: "RSA",
|
||||
cryptoAlgorithm: "RS256",
|
||||
bitSize: 4096,
|
||||
expireInYears: 20,
|
||||
publicKey: "",
|
||||
@ -131,7 +131,7 @@ class CertListPage extends BaseListPage {
|
||||
key: 'cryptoAlgorithm',
|
||||
filterMultiple: false,
|
||||
filters: [
|
||||
{text: 'RSA', value: 'RSA'},
|
||||
{text: 'RS256', value: 'RS256'},
|
||||
],
|
||||
width: '190px',
|
||||
sorter: true,
|
||||
|
@ -27,7 +27,6 @@ export const CropperDiv = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
||||
const {title} = props;
|
||||
const {user} = props;
|
||||
const {account} = props;
|
||||
const {buttonText} = props;
|
||||
let uploadButton;
|
||||
|
||||
|
208
web/src/ModelEditPage.js
Normal file
208
web/src/ModelEditPage.js
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
|
||||
import * as ModelBackend from "./backend/ModelBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
class ModelEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
modelName: props.match.params.modelName,
|
||||
model: null,
|
||||
organizations: [],
|
||||
users: [],
|
||||
models: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getModel();
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getModel() {
|
||||
ModelBackend.getModel(this.state.organizationName, this.state.modelName)
|
||||
.then((model) => {
|
||||
this.setState({
|
||||
model: model,
|
||||
});
|
||||
|
||||
this.getModels(model.owner);
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizations("admin")
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
organizations: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getModels(organizationName) {
|
||||
ModelBackend.getModels(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
models: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseModelField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateModelField(key, value) {
|
||||
value = this.parseModelField(key, value);
|
||||
|
||||
let model = this.state.model;
|
||||
model[key] = value;
|
||||
this.setState({
|
||||
model: model,
|
||||
});
|
||||
}
|
||||
|
||||
renderModel() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("model:New Model") : i18next.t("model:Edit Model")}
|
||||
<Button onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
|
||||
<Row style={{marginTop: '10px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.model.owner} onChange={(value => {this.updateModelField('owner', value);})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</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.model.name} onChange={e => {
|
||||
this.updateModelField('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.model.displayName} onChange={e => {
|
||||
this.updateModelField('displayName', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("model:Model"), i18next.t("model:Model - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22}>
|
||||
<TextArea rows={10} value={this.state.model.modelText} onChange={e => {
|
||||
this.updateModelField('modelText', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.model.isEnabled} onChange={checked => {
|
||||
this.updateModelField('isEnabled', checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
submitModelEdit(willExist) {
|
||||
let model = Setting.deepCopy(this.state.model);
|
||||
ModelBackend.updateModel(this.state.organizationName, this.state.modelName, model)
|
||||
.then((res) => {
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", `Successfully saved`);
|
||||
this.setState({
|
||||
modelName: this.state.model.name,
|
||||
});
|
||||
|
||||
if (willExist) {
|
||||
this.props.history.push(`/models`);
|
||||
} else {
|
||||
this.props.history.push(`/models/${this.state.model.owner}/${this.state.model.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
this.updateModelField('name', this.state.modelName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Failed to connect to server: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteModel() {
|
||||
ModelBackend.deleteModel(this.state.model)
|
||||
.then(() => {
|
||||
this.props.history.push(`/models`);
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Model failed to delete: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.model !== null ? this.renderModel() : null
|
||||
}
|
||||
<div style={{marginTop: '20px', marginLeft: '40px'}}>
|
||||
<Button size="large" onClick={() => this.submitModelEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitModelEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteModel()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModelEditPage;
|
201
web/src/ModelListPage.js
Normal file
201
web/src/ModelListPage.js
Normal file
@ -0,0 +1,201 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Popconfirm, Switch, Table} from 'antd';
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as ModelBackend from "./backend/ModelBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class ModelListPage extends BaseListPage {
|
||||
newModel() {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
owner: "built-in",
|
||||
name: `model_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Model - ${randomName}`,
|
||||
modelText: "",
|
||||
isEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
addModel() {
|
||||
const newModel = this.newModel();
|
||||
ModelBackend.addModel(newModel)
|
||||
.then((res) => {
|
||||
this.props.history.push({pathname: `/models/${newModel.owner}/${newModel.name}`, mode: "add"});
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Model failed to add: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteModel(i) {
|
||||
ModelBackend.deleteModel(this.state.data[i])
|
||||
.then((res) => {
|
||||
Setting.showMessage("success", `Model deleted successfully`);
|
||||
this.setState({
|
||||
data: Setting.deleteRow(this.state.data, i),
|
||||
pagination: {total: this.state.pagination.total - 1},
|
||||
});
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `Model failed to delete: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(models) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: 'owner',
|
||||
key: 'owner',
|
||||
width: '120px',
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps('owner'),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '150px',
|
||||
fixed: 'left',
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps('name'),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/models/${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: '200px',
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps('displayName'),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Is enabled"),
|
||||
dataIndex: 'isEnabled',
|
||||
key: 'isEnabled',
|
||||
width: '120px',
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Switch disabled checkedChildren="ON" unCheckedChildren="OFF" checked={text}/>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
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(`/models/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete model: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteModel(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={models} rowKey="name" size="middle" bordered
|
||||
pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Models")}
|
||||
<Button type="primary" size="small"
|
||||
onClick={this.addModel.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});
|
||||
ModelBackend.getModels("", 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 ModelListPage;
|
@ -155,7 +155,7 @@ class OrganizationEditPage extends React.Component {
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField('passwordType', value);})}>
|
||||
{
|
||||
['plain', 'salt', 'md5-salt', 'bcrypt', 'pbkdf2-salt']
|
||||
['plain', 'salt', 'md5-salt', 'bcrypt', 'pbkdf2-salt', 'argon2id']
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
@ -240,6 +240,16 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("organization:Is profile public"), i18next.t("organization:Is profile public - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.organization.isProfilePublic} onChange={checked => {
|
||||
this.updateOrganizationField('isProfilePublic', checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}}>
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :
|
||||
|
@ -39,6 +39,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
tags: [],
|
||||
masterPassword: "",
|
||||
enableSoftDeletion: false,
|
||||
isProfilePublic: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,11 +13,14 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row} from 'antd';
|
||||
import {Button, Card, Col, Descriptions, Input, Modal, Row, Select} from 'antd';
|
||||
import {InfoCircleTwoTone} from "@ant-design/icons";
|
||||
import * as PaymentBackend from "./backend/PaymentBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
class PaymentEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -26,6 +29,8 @@ class PaymentEditPage extends React.Component {
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
paymentName: props.match.params.paymentName,
|
||||
payment: null,
|
||||
isModalVisible: false,
|
||||
isInvoiceLoading: false,
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
@ -40,6 +45,8 @@ class PaymentEditPage extends React.Component {
|
||||
this.setState({
|
||||
payment: payment,
|
||||
});
|
||||
|
||||
Setting.scrollToDiv("invoice-area");
|
||||
});
|
||||
}
|
||||
|
||||
@ -60,6 +67,82 @@ class PaymentEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
issueInvoice() {
|
||||
this.setState({
|
||||
isModalVisible: false,
|
||||
isInvoiceLoading: true,
|
||||
});
|
||||
|
||||
PaymentBackend.invoicePayment(this.state.payment.owner, this.state.paymentName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
isInvoiceLoading: false,
|
||||
});
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", `Successfully invoiced`);
|
||||
Setting.openLinkSafe(res.data);
|
||||
this.getPayment();
|
||||
} else {
|
||||
Setting.showMessage(res.msg.includes("成功") ? "info" : "error", res.msg);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
isInvoiceLoading: false,
|
||||
});
|
||||
Setting.showMessage("error", `Failed to connect to server: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
downloadInvoice() {
|
||||
Setting.openLinkSafe(this.state.payment.invoiceUrl);
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
const ths = this;
|
||||
const handleIssueInvoice = () => {
|
||||
ths.issueInvoice();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
this.setState({
|
||||
isModalVisible: false,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={
|
||||
<div>
|
||||
<InfoCircleTwoTone twoToneColor="rgb(45,120,213)" />
|
||||
{" " + i18next.t("payment:Confirm your invoice information")}
|
||||
</div>
|
||||
}
|
||||
visible={this.state.isModalVisible}
|
||||
onOk={handleIssueInvoice}
|
||||
onCancel={handleCancel}
|
||||
okText={i18next.t("payment:Issue Invoice")}
|
||||
cancelText={i18next.t("general:Cancel")}>
|
||||
<p>
|
||||
{
|
||||
i18next.t("payment:Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.")
|
||||
}
|
||||
<br/>
|
||||
<br/>
|
||||
<Descriptions size={"small"} bordered>
|
||||
<Descriptions.Item label={i18next.t("payment:Person name")} span={3}>{this.state.payment?.personName}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Person ID card")} span={3}>{this.state.payment?.personIdCard}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Person Email")} span={3}>{this.state.payment?.personEmail}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Person phone")} span={3}>{this.state.payment?.personPhone}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Invoice type")} span={3}>{this.state.payment?.invoiceType === "Individual" ? i18next.t("payment:Individual") : i18next.t("payment:Organization")}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Invoice title")} span={3}>{this.state.payment?.invoiceTitle}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Invoice tax ID")} span={3}>{this.state.payment?.invoiceTaxId}</Descriptions.Item>
|
||||
<Descriptions.Item label={i18next.t("payment:Invoice remark")} span={3}>{this.state.payment?.invoiceRemark}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</p>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
renderPayment() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
@ -75,7 +158,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.organization} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.organization} onChange={e => {
|
||||
// this.updatePaymentField('organization', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -85,7 +168,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.name} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.name} onChange={e => {
|
||||
// this.updatePaymentField('name', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -95,7 +178,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.displayName} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.displayName} onChange={e => {
|
||||
this.updatePaymentField('displayName', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -105,7 +188,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.provider} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.provider} onChange={e => {
|
||||
// this.updatePaymentField('provider', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -115,7 +198,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Type"), i18next.t("payment:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.type} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.type} onChange={e => {
|
||||
// this.updatePaymentField('type', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -125,7 +208,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.productName} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.productName} onChange={e => {
|
||||
// this.updatePaymentField('productName', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -135,7 +218,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Price"), i18next.t("payment:Price - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.price} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.price} onChange={e => {
|
||||
// this.updatePaymentField('amount', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -145,7 +228,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.currency} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.currency} onChange={e => {
|
||||
// this.updatePaymentField('currency', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -155,7 +238,7 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:State"), i18next.t("payment:State - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.state} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.state} onChange={e => {
|
||||
// this.updatePaymentField('state', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
@ -165,18 +248,200 @@ class PaymentEditPage extends React.Component {
|
||||
{Setting.getLabel(i18next.t("payment:Message"), i18next.t("payment:Message - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.payment.message} onChange={e => {
|
||||
<Input disabled={true} value={this.state.payment.message} onChange={e => {
|
||||
// this.updatePaymentField('message', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Person name"), i18next.t("payment:Person name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personName} onChange={e => {
|
||||
this.updatePaymentField('personName', e.target.value);
|
||||
if (this.state.payment.invoiceType === "Individual") {
|
||||
this.updatePaymentField('invoiceTitle', e.target.value);
|
||||
this.updatePaymentField('invoiceTaxId', "");
|
||||
}
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Person ID card"), i18next.t("payment:Person ID card - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personIdCard} onChange={e => {
|
||||
this.updatePaymentField('personIdCard', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Person Email"), i18next.t("payment:Person Email - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personEmail} onChange={e => {
|
||||
this.updatePaymentField('personEmail', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Person phone"), i18next.t("payment:Person phone - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.personPhone} onChange={e => {
|
||||
this.updatePaymentField('personPhone', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Invoice type"), i18next.t("payment:Invoice type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select disabled={this.state.payment.invoiceUrl !== ""} virtual={false} style={{width: '100%'}} value={this.state.payment.invoiceType} onChange={(value => {
|
||||
this.updatePaymentField('invoiceType', value);
|
||||
if (value === "Individual") {
|
||||
this.updatePaymentField('invoiceTitle', this.state.payment.personName);
|
||||
this.updatePaymentField('invoiceTaxId', "");
|
||||
}
|
||||
})}>
|
||||
{
|
||||
[
|
||||
{id: 'Individual', name: i18next.t("payment:Individual")},
|
||||
{id: 'Organization', name: i18next.t("payment:Organization")},
|
||||
].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("payment:Invoice title"), i18next.t("payment:Invoice title - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== "" || this.state.payment.invoiceType === "Individual"} value={this.state.payment.invoiceTitle} onChange={e => {
|
||||
this.updatePaymentField('invoiceTitle', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Invoice tax ID"), i18next.t("payment:Invoice tax ID - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== "" || this.state.payment.invoiceType === "Individual"} value={this.state.payment.invoiceTaxId} onChange={e => {
|
||||
this.updatePaymentField('invoiceTaxId', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Invoice remark"), i18next.t("payment:Invoice remark - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={this.state.payment.invoiceUrl !== ""} value={this.state.payment.invoiceRemark} onChange={e => {
|
||||
this.updatePaymentField('invoiceRemark', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Invoice URL"), i18next.t("payment:Invoice URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input disabled={true} value={this.state.payment.invoiceUrl} onChange={e => {
|
||||
this.updatePaymentField('invoiceUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row id={"invoice-area"} style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("payment:Invoice actions"), i18next.t("payment:Invoice actions - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
{
|
||||
this.state.payment.invoiceUrl === "" ? (
|
||||
<Button type={"primary"} loading={this.state.isInvoiceLoading} onClick={() => {
|
||||
const errorText = this.checkError();
|
||||
if (errorText !== "") {
|
||||
Setting.showMessage("error", errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isModalVisible: true,
|
||||
});
|
||||
}}>{i18next.t("payment:Issue Invoice")}</Button>
|
||||
) : (
|
||||
<Button type={"primary"} onClick={() => this.downloadInvoice(false)}>{i18next.t("payment:Download Invoice")}</Button>
|
||||
)
|
||||
}
|
||||
<Button style={{marginLeft: "20px"}} onClick={() => Setting.goToLink(this.state.payment.returnUrl)}>{i18next.t("payment:Return to Website")}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
checkError() {
|
||||
if (this.state.payment.state !== "Paid") {
|
||||
return i18next.t("payment:Please pay the order first!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidPersonName(this.state.payment.personName)) {
|
||||
return i18next.t("signup:Please input your real name!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidIdCard(this.state.payment.personIdCard)) {
|
||||
return i18next.t("signup:Please input the correct ID card number!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidEmail(this.state.payment.personEmail)) {
|
||||
return i18next.t("signup:The input is not valid Email!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidPhone(this.state.payment.personPhone)) {
|
||||
return i18next.t("signup:The input is not valid Phone!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidPhone(this.state.payment.personPhone)) {
|
||||
return i18next.t("signup:The input is not valid Phone!");
|
||||
}
|
||||
|
||||
if (this.state.payment.invoiceType === "Individual") {
|
||||
if (this.state.payment.invoiceTitle !== this.state.payment.personName) {
|
||||
return i18next.t("signup:The input is not invoice title!");
|
||||
}
|
||||
|
||||
if (this.state.payment.invoiceTaxId !== "") {
|
||||
return i18next.t("signup:The input is not invoice Tax ID!");
|
||||
}
|
||||
} else {
|
||||
if (!Setting.isValidInvoiceTitle(this.state.payment.invoiceTitle)) {
|
||||
return i18next.t("signup:The input is not invoice title!");
|
||||
}
|
||||
|
||||
if (!Setting.isValidTaxId(this.state.payment.invoiceTaxId)) {
|
||||
return i18next.t("signup:The input is not invoice Tax ID!");
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
submitPaymentEdit(willExist) {
|
||||
const errorText = this.checkError();
|
||||
if (errorText !== "") {
|
||||
Setting.showMessage("error", errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
let payment = Setting.deepCopy(this.state.payment);
|
||||
PaymentBackend.updatePayment(this.state.organizationName, this.state.paymentName, payment)
|
||||
PaymentBackend.updatePayment(this.state.payment.owner, this.state.paymentName, payment)
|
||||
.then((res) => {
|
||||
if (res.msg === "") {
|
||||
Setting.showMessage("success", `Successfully saved`);
|
||||
@ -215,6 +480,9 @@ class PaymentEditPage extends React.Component {
|
||||
{
|
||||
this.state.payment !== null ? this.renderPayment() : null
|
||||
}
|
||||
{
|
||||
this.renderModal()
|
||||
}
|
||||
<div style={{marginTop: '20px', marginLeft: '40px'}}>
|
||||
<Button size="large" onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
|
@ -20,6 +20,7 @@ import * as UserBackend from "./backend/UserBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
import * as RoleBackend from "./backend/RoleBackend";
|
||||
import * as ModelBackend from "./backend/ModelBackend";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@ -34,6 +35,7 @@ class PermissionEditPage extends React.Component {
|
||||
organizations: [],
|
||||
users: [],
|
||||
roles: [],
|
||||
models: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
@ -52,6 +54,7 @@ class PermissionEditPage extends React.Component {
|
||||
|
||||
this.getUsers(permission.owner);
|
||||
this.getRoles(permission.owner);
|
||||
this.getModels(permission.owner);
|
||||
});
|
||||
}
|
||||
|
||||
@ -82,6 +85,15 @@ class PermissionEditPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
getModels(organizationName) {
|
||||
ModelBackend.getModels(organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
models: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parsePermissionField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
@ -146,6 +158,20 @@ class PermissionEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: '100%'}} value={this.state.permission.model} onChange={(model => {
|
||||
this.updatePermissionField('model', model);
|
||||
})}>
|
||||
{
|
||||
this.state.models.map((model, index) => <Option key={index} value={model.name}>{model.name}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :
|
||||
|
@ -17,7 +17,6 @@ import {Button, Descriptions, Spin} from "antd";
|
||||
import i18next from "i18next";
|
||||
import * as ProductBackend from "./backend/ProductBackend";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
import * as Provider from "./auth/Provider";
|
||||
import * as Setting from "./Setting";
|
||||
|
||||
class ProductBuyPage extends React.Component {
|
||||
@ -148,7 +147,7 @@ class ProductBuyPage extends React.Component {
|
||||
|
||||
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} />
|
||||
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
} size={"large"} >
|
||||
{
|
||||
text
|
||||
|
@ -212,6 +212,12 @@ class ProviderEditPage extends React.Component {
|
||||
if (value === "Local File System") {
|
||||
this.updateProviderField('domain', Setting.getFullServerUrl());
|
||||
}
|
||||
if (value === "Custom") {
|
||||
this.updateProviderField('customAuthUrl', 'https://door.casdoor.com/login/oauth/authorize');
|
||||
this.updateProviderField('customScope', 'openid profile email');
|
||||
this.updateProviderField('customTokenUrl', 'https://door.casdoor.com/api/login/oauth/access_token');
|
||||
this.updateProviderField('customUserInfoUrl', 'https://door.casdoor.com/api/userinfo');
|
||||
}
|
||||
})}>
|
||||
{
|
||||
Setting.getProviderTypeOptions(this.state.provider.category).map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
|
||||
@ -256,6 +262,79 @@ class ProviderEditPage extends React.Component {
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.type !== "Custom" ? null : (
|
||||
<React.Fragment>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customAuthUrl} onChange={e => {
|
||||
this.updateProviderField('customAuthUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customScope} onChange={e => {
|
||||
this.updateProviderField('customScope', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:Token URL"), i18next.t("provider:Token URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customTokenUrl} onChange={e => {
|
||||
this.updateProviderField('customTokenUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("provider:UserInfo URL"), i18next.t("provider:UserInfo URL - Tooltip"))}
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.provider.customUserInfoUrl} onChange={e => {
|
||||
this.updateProviderField('customUserInfoUrl', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel( i18next.t("general:Favicon"), i18next.t("general:Favicon - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<Input prefix={<LinkOutlined/>} value={this.state.provider.customLogo} onChange={e => {
|
||||
this.updateProviderField('customLogo', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{i18next.t("general:Preview")}:
|
||||
</Col>
|
||||
<Col span={23} >
|
||||
<a target="_blank" rel="noreferrer" href={this.state.provider.customLogo}>
|
||||
<img src={this.state.provider.customLogo} alt={this.state.provider.customLogo} height={90} style={{marginBottom: '20px'}}/>
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{this.getClientIdLabel()}
|
||||
@ -303,7 +382,7 @@ class ProviderEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.provider.type !== "Adfs" && this.state.provider.type !== "Casdoor" ? null : (
|
||||
this.state.provider.type !== "Adfs" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={2}>
|
||||
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
|
||||
|
@ -74,7 +74,7 @@ class ResourceListPage extends BaseListPage {
|
||||
|
||||
renderUpload() {
|
||||
return (
|
||||
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx" showUploadList={false}
|
||||
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
|
||||
beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}>
|
||||
<Button icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
|
||||
{i18next.t("resource:Upload a file...")}
|
||||
|
@ -32,6 +32,83 @@ export const StaticBaseUrl = "https://cdn.casbin.org";
|
||||
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
|
||||
export const CountryRegionData = getCountryRegionData();
|
||||
|
||||
export const OtherProviderInfo = {
|
||||
SMS: {
|
||||
"Aliyun SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/sms",
|
||||
},
|
||||
"Tencent Cloud SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/sms",
|
||||
},
|
||||
"Volc Engine SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_volc_engine.jpg`,
|
||||
url: "https://www.volcengine.com/products/cloud-sms",
|
||||
},
|
||||
"Huawei Cloud SMS": {
|
||||
logo: `${StaticBaseUrl}/img/social_huawei.png`,
|
||||
url: "https://www.huaweicloud.com/product/msgsms.html",
|
||||
},
|
||||
},
|
||||
Email: {
|
||||
"Default": {
|
||||
logo: `${StaticBaseUrl}/img/social_default.png`,
|
||||
url: "",
|
||||
},
|
||||
},
|
||||
Storage: {
|
||||
"Local File System": {
|
||||
logo: `${StaticBaseUrl}/img/social_file.png`,
|
||||
url: "",
|
||||
},
|
||||
"AWS S3": {
|
||||
logo: `${StaticBaseUrl}/img/social_aws.png`,
|
||||
url: "https://aws.amazon.com/s3",
|
||||
},
|
||||
"Aliyun OSS": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/oss",
|
||||
},
|
||||
"Tencent Cloud COS": {
|
||||
logo: `${StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/cos",
|
||||
},
|
||||
"Azure Blob": {
|
||||
logo: `${StaticBaseUrl}/img/social_azure.jpg`,
|
||||
url: "https://azure.microsoft.com/en-us/services/storage/blobs/"
|
||||
}
|
||||
},
|
||||
SAML: {
|
||||
"Aliyun IDaaS": {
|
||||
logo: `${StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/idaas"
|
||||
},
|
||||
"Keycloak": {
|
||||
logo: `${StaticBaseUrl}/img/social_keycloak.png`,
|
||||
url: "https://www.keycloak.org/"
|
||||
},
|
||||
},
|
||||
Payment: {
|
||||
"Alipay": {
|
||||
logo: `${StaticBaseUrl}/img/payment_alipay.png`,
|
||||
url: "https://www.alipay.com/"
|
||||
},
|
||||
"WeChat Pay": {
|
||||
logo: `${StaticBaseUrl}/img/payment_wechat_pay.png`,
|
||||
url: "https://pay.weixin.qq.com/"
|
||||
},
|
||||
"PayPal": {
|
||||
logo: `${StaticBaseUrl}/img/payment_paypal.png`,
|
||||
url: "https://www.paypal.com/"
|
||||
},
|
||||
"GC": {
|
||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getCountryRegionData() {
|
||||
let language = i18next.language;
|
||||
if (language === null || language === "null") {
|
||||
@ -115,6 +192,17 @@ export function getSignupItem(application, itemName) {
|
||||
return signupItems[0];
|
||||
}
|
||||
|
||||
export function isValidPersonName(personName) {
|
||||
// https://blog.css8.cn/post/14210975.html
|
||||
const personNameRegex = /^[\u4e00-\u9fa5]{2,6}$/;
|
||||
return personNameRegex.test(personName);
|
||||
}
|
||||
|
||||
export function isValidIdCard(idCard) {
|
||||
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9X]$/;
|
||||
return idCardRegex.test(idCard);
|
||||
}
|
||||
|
||||
export function isValidEmail(email) {
|
||||
// https://github.com/yiminghe/async-validator/blob/057b0b047f88fac65457bae691d6cb7c6fe48ce1/src/rule/type.ts#L9
|
||||
const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
@ -122,11 +210,36 @@ export function isValidEmail(email) {
|
||||
}
|
||||
|
||||
export function isValidPhone(phone) {
|
||||
if (phone === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://learnku.com/articles/31543, `^s*$` filter empty email individually.
|
||||
const phoneRegex = /^\s*$|^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
|
||||
export function isValidInvoiceTitle(invoiceTitle) {
|
||||
if (invoiceTitle === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://blog.css8.cn/post/14210975.html
|
||||
const invoiceTitleRegex = /^[\(\)\(\)\u4e00-\u9fa5]{0,50}$/;
|
||||
return invoiceTitleRegex.test(invoiceTitle);
|
||||
}
|
||||
|
||||
export function isValidTaxId(taxId) {
|
||||
// https://www.codetd.com/article/8592083
|
||||
const regArr = [/^[\da-z]{10,15}$/i, /^\d{6}[\da-z]{10,12}$/i, /^[a-z]\d{6}[\da-z]{9,11}$/i, /^[a-z]{2}\d{6}[\da-z]{8,10}$/i, /^\d{14}[\dx][\da-z]{4,5}$/i, /^\d{17}[\dx][\da-z]{1,2}$/i, /^[a-z]\d{14}[\dx][\da-z]{3,4}$/i, /^[a-z]\d{17}[\dx][\da-z]{0,1}$/i, /^[\d]{6}[\da-z]{13,14}$/i];
|
||||
for (let i = 0; i < regArr.length; i++) {
|
||||
if (regArr[i].test(taxId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isAffiliationPrompted(application) {
|
||||
const signupItem = getSignupItem(application, "Affiliation");
|
||||
if (signupItem === null) {
|
||||
@ -199,11 +312,25 @@ export function openLink(link) {
|
||||
w.location.href = link;
|
||||
}
|
||||
|
||||
export function openLinkSafe(link) {
|
||||
// Javascript window.open issue in safari
|
||||
// https://stackoverflow.com/questions/45569893/javascript-window-open-issue-in-safari
|
||||
let a = document.createElement('a');
|
||||
a.href = link;
|
||||
a.setAttribute('target', '_blank');
|
||||
a.click();
|
||||
}
|
||||
|
||||
export function goToLink(link) {
|
||||
window.location.href = link;
|
||||
}
|
||||
|
||||
export function goToLinkSoft(ths, link) {
|
||||
if (link.startsWith("http")) {
|
||||
openLink(link);
|
||||
return;
|
||||
}
|
||||
|
||||
ths.props.history.push(link);
|
||||
}
|
||||
|
||||
@ -214,6 +341,8 @@ export function showMessage(type, text) {
|
||||
message.success(text);
|
||||
} else if (type === "error") {
|
||||
message.error(text);
|
||||
} else if (type === "info") {
|
||||
message.info(text);
|
||||
}
|
||||
}
|
||||
|
||||
@ -380,9 +509,20 @@ export function getClickable(text) {
|
||||
)
|
||||
}
|
||||
|
||||
export function getProviderLogoURL(provider) {
|
||||
if (provider.category === "OAuth") {
|
||||
if (provider.type === "Custom") {
|
||||
return provider.customLogo;
|
||||
}
|
||||
return `${StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`;
|
||||
} else {
|
||||
return OtherProviderInfo[provider.category][provider.type].logo;
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderLogo(provider) {
|
||||
const idp = provider.type.toLowerCase().trim().split(' ')[0];
|
||||
const url = `${StaticBaseUrl}/img/social_${idp}.png`;
|
||||
const url = getProviderLogoURL(provider);
|
||||
return (
|
||||
<img width={30} height={30} src={url} alt={idp} />
|
||||
)
|
||||
@ -414,6 +554,10 @@ export function getProviderTypeOptions(category) {
|
||||
{id: 'AzureAD', name: 'AzureAD'},
|
||||
{id: 'Slack', name: 'Slack'},
|
||||
{id: 'Steam', name: 'Steam'},
|
||||
{id: 'Bilibili', name: 'Bilibili'},
|
||||
{id: 'Okta', name: 'Okta'},
|
||||
{id: 'Douyin', name: 'Douyin'},
|
||||
{id: 'Custom', name: 'Custom'},
|
||||
]
|
||||
);
|
||||
} else if (category === "Email") {
|
||||
@ -438,6 +582,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: 'AWS S3', name: 'AWS S3'},
|
||||
{id: 'Aliyun OSS', name: 'Aliyun OSS'},
|
||||
{id: 'Tencent Cloud COS', name: 'Tencent Cloud COS'},
|
||||
{id: 'Azure Blob', name: 'Azure Blob'}
|
||||
]
|
||||
);
|
||||
} else if (category === "SAML") {
|
||||
@ -660,6 +805,15 @@ export function getFromLink() {
|
||||
return from;
|
||||
}
|
||||
|
||||
export function scrollToDiv(divId) {
|
||||
if (divId) {
|
||||
let ele = document.getElementById(divId);
|
||||
if (ele) {
|
||||
ele.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getSyncerTableColumns(syncer) {
|
||||
switch (syncer.type) {
|
||||
case "Keycloak":
|
||||
@ -683,7 +837,7 @@ export function getSyncerTableColumns(syncer) {
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"USERNAME",
|
||||
"name":"LAST_NAME+FIRST_NAME",
|
||||
"type":"string",
|
||||
"casdoorName":"DisplayName",
|
||||
"isHashed":true,
|
||||
|
@ -159,7 +159,7 @@ class SignupTable extends React.Component {
|
||||
title: i18next.t("provider:rule"),
|
||||
dataIndex: 'rule',
|
||||
key: 'rule',
|
||||
width: '120px',
|
||||
width: '155px',
|
||||
render: (text, record, index) => {
|
||||
let options = [];
|
||||
if (record.name === "ID") {
|
||||
@ -167,12 +167,17 @@ class SignupTable extends React.Component {
|
||||
{id: 'Random', name: 'Random'},
|
||||
{id: 'Incremental', name: 'Incremental'},
|
||||
];
|
||||
} if (record.name === "Display name") {
|
||||
} else if (record.name === "Display name") {
|
||||
options = [
|
||||
{id: 'None', name: 'None'},
|
||||
{id: 'Real name', name: 'Real name'},
|
||||
{id: 'First, last', name: 'First, last'},
|
||||
];
|
||||
} else if (record.name === "Email") {
|
||||
options = [
|
||||
{id: 'Normal', name: 'Normal'},
|
||||
{id: 'No verification', name: 'No verification'},
|
||||
];
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
|
@ -73,6 +73,20 @@ class SyncerListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
|
||||
runSyncer(i) {
|
||||
this.setState({loading: true});
|
||||
SyncerBackend.runSyncer("admin", this.state.data[i].name)
|
||||
.then((res) => {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("success", `Syncer sync users successfully`);
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
this.setState({loading: false});
|
||||
Setting.showMessage("error", `Syncer failed to sync users: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(syncers) {
|
||||
const columns = [
|
||||
{
|
||||
@ -205,12 +219,13 @@ class SyncerListPage extends BaseListPage {
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: '',
|
||||
key: 'op',
|
||||
width: '170px',
|
||||
width: '240px',
|
||||
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(`/syncers/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.runSyncer(index)}>{i18next.t("general:Sync")}</Button>
|
||||
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => this.props.history.push(`/syncers/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<Popconfirm
|
||||
title={`Sure to delete syncer: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteSyncer(index)}
|
||||
|
@ -19,7 +19,6 @@ import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as TokenBackend from "./backend/TokenBackend";
|
||||
import i18next from "i18next";
|
||||
import * as ResourceBackend from "./backend/ResourceBackend";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
|
||||
class TokenListPage extends BaseListPage {
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, Row, Select, Switch} from 'antd';
|
||||
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from 'antd';
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
@ -47,6 +47,7 @@ class UserEditPage extends React.Component {
|
||||
organizations: [],
|
||||
applications: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -59,9 +60,14 @@ class UserEditPage extends React.Component {
|
||||
|
||||
getUser() {
|
||||
UserBackend.getUser(this.state.organizationName, this.state.userName)
|
||||
.then((user) => {
|
||||
.then((data) => {
|
||||
if (data.status === null || data.status !== "error") {
|
||||
this.setState({
|
||||
user: data,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
user: user,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -423,6 +429,8 @@ class UserEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
</Card>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ -469,13 +477,24 @@ class UserEditPage extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.user !== null ? this.renderUser() : null
|
||||
this.state.loading ? <Spin loading={this.state.loading} size="large" /> : (
|
||||
this.state.user !== null ? this.renderUser() :
|
||||
<Result
|
||||
status="404"
|
||||
title="404 NOT FOUND"
|
||||
subTitle={i18next.t("general:Sorry, the user you visited does not exist or you are not authorized to access this user.")}
|
||||
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.user === null ? null :
|
||||
<div style={{marginTop: '20px', marginLeft: '40px'}}>
|
||||
<Button size="large" onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
}
|
||||
<div style={{marginTop: '20px', marginLeft: '40px'}}>
|
||||
<Button size="large" onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} size="large" onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import * as Setting from "./Setting";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import * as path from "path";
|
||||
|
||||
class UserListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
|
32
web/src/auth/BilibiliLoginButton.js
Normal file
32
web/src/auth/BilibiliLoginButton.js
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {createButton} from "react-social-login-buttons";
|
||||
import {StaticBaseUrl} from "../Setting";
|
||||
|
||||
function Icon({ width = 24, height = 24, color }) {
|
||||
return <img src={`${StaticBaseUrl}/buttons/bilibili.svg`} alt="Sign in with Bilibili"/>;
|
||||
}
|
||||
|
||||
const config = {
|
||||
text: "Sign in with Bilibili",
|
||||
icon: Icon,
|
||||
iconFormat: name => `fa fa-${name}`,
|
||||
style: {background: "#0191e0"},
|
||||
activeStyle: {background: "rgb(76,143,208)"},
|
||||
};
|
||||
|
||||
const BilibiliLoginButton = createButton(config);
|
||||
|
||||
export default BilibiliLoginButton;
|
32
web/src/auth/DouyinLoginButton.js
Normal file
32
web/src/auth/DouyinLoginButton.js
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 {createButton} from "react-social-login-buttons";
|
||||
import {StaticBaseUrl} from "../Setting";
|
||||
|
||||
function Icon({width = 24, height = 24, color}) {
|
||||
return <img src={`${StaticBaseUrl}/buttons/douyin.svg`} alt="Sign in with Douyin" style={{width: 24, height: 24}}/>;
|
||||
}
|
||||
|
||||
const config = {
|
||||
text: "Sign in with Douyin",
|
||||
icon: Icon,
|
||||
iconFormat: name => `fa fa-${name}`,
|
||||
style: {background: "#ffffff", color: "#000000"},
|
||||
activeStyle: {background: "#ededee"},
|
||||
};
|
||||
|
||||
const DouyinLoginButton = createButton(config);
|
||||
|
||||
export default DouyinLoginButton;
|
@ -43,8 +43,11 @@ import AppleLoginButton from "./AppleLoginButton"
|
||||
import AzureADLoginButton from "./AzureADLoginButton";
|
||||
import SlackLoginButton from "./SlackLoginButton";
|
||||
import SteamLoginButton from "./SteamLoginButton";
|
||||
import OktaLoginButton from "./OktaLoginButton";
|
||||
import DouyinLoginButton from "./DouyinLoginButton";
|
||||
import CustomGithubCorner from "../CustomGithubCorner";
|
||||
import {CountDownInput} from "../common/CountDownInput";
|
||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props) {
|
||||
@ -149,7 +152,7 @@ class LoginPage extends React.Component {
|
||||
AuthBackend.loginCas(values, casParams).then((res) => {
|
||||
if (res.status === 'ok') {
|
||||
let msg = "Logged in successfully. "
|
||||
if (casParams.service == "") {
|
||||
if (casParams.service === "") {
|
||||
//If service was not specified, CAS MUST display a message notifying the client that it has successfully initiated a single sign-on session.
|
||||
msg += "Now you can visit apps protected by casdoor."
|
||||
}
|
||||
@ -278,6 +281,12 @@ class LoginPage extends React.Component {
|
||||
return <SlackLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Steam") {
|
||||
return <SteamLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Bilibili") {
|
||||
return <BilibiliLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Okta") {
|
||||
return <OktaLoginButton text={text} align={"center"} />
|
||||
} else if (type === "Douyin") {
|
||||
return <DouyinLoginButton text={text} align={"center"} />
|
||||
}
|
||||
|
||||
return text;
|
||||
@ -305,13 +314,13 @@ class LoginPage extends React.Component {
|
||||
if (provider.category === "OAuth") {
|
||||
return (
|
||||
<a key={provider.displayName} href={Provider.getAuthUrl(application, provider, "signup")}>
|
||||
<img width={width} height={width} src={Provider.getProviderLogo(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
<img width={width} height={width} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</a>
|
||||
)
|
||||
} else if (provider.category === "SAML") {
|
||||
return (
|
||||
<a key={provider.displayName} onClick={this.getSamlUrl.bind(this, provider)}>
|
||||
<img width={width} height={width} src={Provider.getProviderLogo(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
<img width={width} height={width} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} style={{margin: margin}} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@ -593,6 +602,9 @@ class LoginPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/*{*/}
|
||||
{/* JSON.stringify(silentSignin)*/}
|
||||
{/*}*/}
|
||||
<div style={{fontSize: 16, textAlign: "left"}}>
|
||||
{i18next.t("login:Continue with")} :
|
||||
</div>
|
||||
|
32
web/src/auth/OktaLoginButton.js
Normal file
32
web/src/auth/OktaLoginButton.js
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 {createButton} from "react-social-login-buttons";
|
||||
import {StaticBaseUrl} from "../Setting";
|
||||
|
||||
function Icon({ width = 24, height = 24, color }) {
|
||||
return <img src={`${StaticBaseUrl}/buttons/okta.svg`} alt="Sign in with Okta" style={{width: 24, height: 24}} />;
|
||||
}
|
||||
|
||||
const config = {
|
||||
text: "Sign in with Okta",
|
||||
icon: Icon,
|
||||
iconFormat: name => `fa fa-${name}`,
|
||||
style: {background: "#ffffff", color: "#000000"},
|
||||
activeStyle: {background: "#ededee"},
|
||||
};
|
||||
|
||||
const OktaLoginButton = createButton(config);
|
||||
|
||||
export default OktaLoginButton;
|
@ -107,88 +107,21 @@ const authInfo = {
|
||||
Steam: {
|
||||
endpoint: "https://steamcommunity.com/openid/login",
|
||||
},
|
||||
};
|
||||
|
||||
const otherProviderInfo = {
|
||||
SMS: {
|
||||
"Aliyun SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/sms",
|
||||
},
|
||||
"Tencent Cloud SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/sms",
|
||||
},
|
||||
"Volc Engine SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_volc_engine.jpg`,
|
||||
url: "https://www.volcengine.com/products/cloud-sms",
|
||||
},
|
||||
"Huawei Cloud SMS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_huawei.png`,
|
||||
url: "https://www.huaweicloud.com/product/msgsms.html",
|
||||
},
|
||||
Okta: {
|
||||
scope: "openid%20profile%20email",
|
||||
endpoint: "http://example.com",
|
||||
},
|
||||
Email: {
|
||||
"Default": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_default.png`,
|
||||
url: "",
|
||||
},
|
||||
Douyin: {
|
||||
scope: "user_info",
|
||||
endpoint: "https://open.douyin.com/platform/oauth/connect",
|
||||
},
|
||||
Storage: {
|
||||
"Local File System": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_file.png`,
|
||||
url: "",
|
||||
},
|
||||
"AWS S3": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aws.png`,
|
||||
url: "https://aws.amazon.com/s3",
|
||||
},
|
||||
"Aliyun OSS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/oss",
|
||||
},
|
||||
"Tencent Cloud COS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_tencent_cloud.jpg`,
|
||||
url: "https://cloud.tencent.com/product/cos",
|
||||
},
|
||||
Custom: {
|
||||
endpoint: "https://example.com/",
|
||||
},
|
||||
SAML: {
|
||||
"Aliyun IDaaS": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_aliyun.png`,
|
||||
url: "https://aliyun.com/product/idaas"
|
||||
},
|
||||
"Keycloak": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/social_keycloak.png`,
|
||||
url: "https://www.keycloak.org/"
|
||||
},
|
||||
},
|
||||
Payment: {
|
||||
"Alipay": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_alipay.png`,
|
||||
url: "https://www.alipay.com/"
|
||||
},
|
||||
"WeChat Pay": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_wechat_pay.png`,
|
||||
url: "https://pay.weixin.qq.com/"
|
||||
},
|
||||
"PayPal": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_paypal.png`,
|
||||
url: "https://www.paypal.com/"
|
||||
},
|
||||
"GC": {
|
||||
logo: `${Setting.StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getProviderLogo(provider) {
|
||||
if (provider.category === "OAuth") {
|
||||
return `${Setting.StaticBaseUrl}/img/social_${provider.type.toLowerCase()}.png`;
|
||||
} else {
|
||||
return otherProviderInfo[provider.category][provider.type].logo;
|
||||
Bilibili: {
|
||||
endpoint: "https://passport.bilibili.com/register/pc_oauth2.html"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function getProviderUrl(provider) {
|
||||
if (provider.category === "OAuth") {
|
||||
@ -204,7 +137,7 @@ export function getProviderUrl(provider) {
|
||||
|
||||
return `${urlObj.protocol}//${host}`;
|
||||
} else {
|
||||
return otherProviderInfo[provider.category][provider.type].url;
|
||||
return Setting.OtherProviderInfo[provider.category][provider.type].url;
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,14 +151,14 @@ export function getProviderLogoWidget(provider) {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<a target="_blank" rel="noreferrer" href={getProviderUrl(provider)}>
|
||||
<img width={36} height={36} src={getProviderLogo(provider)} alt={provider.displayName} />
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={provider.type}>
|
||||
<img width={36} height={36} src={getProviderLogo(provider)} alt={provider.displayName} />
|
||||
<img width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@ -308,5 +241,13 @@ export function getAuthUrl(application, provider, method) {
|
||||
return `${endpoint}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Steam") {
|
||||
return `${endpoint}?openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns=http://specs.openid.net/auth/2.0&openid.realm=${window.location.origin}&openid.return_to=${redirectUri}?state=${state}`;
|
||||
}
|
||||
} else if (provider.type === "Okta") {
|
||||
return `${provider.domain}/v1/authorize?client_id=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Douyin") {
|
||||
return `${endpoint}?client_key=${provider.clientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code&scope=${scope}`;
|
||||
} else if (provider.type === "Custom") {
|
||||
return `${provider.customAuthUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${provider.customScope}&response_type=code&state=${state}`;
|
||||
} else if (provider.type === "Bilibili") {
|
||||
return `${endpoint}#/?client_id=${provider.clientId}&return_url=${redirectUri}&state=${state}&response_type=code`
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,14 @@ class SelfLoginButton extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
getAccountShowName() {
|
||||
let {name, displayName} = this.props.account;
|
||||
if (displayName !== '') {
|
||||
name += ' (' + displayName + ')';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
render() {
|
||||
const config = {
|
||||
icon: this.generateIcon(),
|
||||
@ -32,7 +40,7 @@ class SelfLoginButton extends React.Component {
|
||||
};
|
||||
|
||||
const SelfLoginButton = createButton(config);
|
||||
return <SelfLoginButton text={`${this.props.account.name} (${this.props.account.displayName})`} onClick={() => this.props.onClick()} align={"center"} />
|
||||
return <SelfLoginButton text={this.getAccountShowName()} onClick={() => this.props.onClick()} align={"center"}/>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,20 +325,23 @@ class SignupPage extends React.Component {
|
||||
>
|
||||
<Input onChange={e => this.setState({email: e.target.value})} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="emailCode"
|
||||
key="emailCode"
|
||||
label={i18next.t("code:Email code")}
|
||||
rules={[{
|
||||
required: required,
|
||||
message: i18next.t("code:Please input your verification code!"),
|
||||
}]}
|
||||
>
|
||||
<CountDownInput
|
||||
disabled={!this.state.validEmail}
|
||||
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationOrgName(application)]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{
|
||||
signupItem.rule !== "No verification" &&
|
||||
<Form.Item
|
||||
name="emailCode"
|
||||
key="emailCode"
|
||||
label={i18next.t("code:Email code")}
|
||||
rules={[{
|
||||
required: required,
|
||||
message: i18next.t("code:Please input your verification code!"),
|
||||
}]}
|
||||
>
|
||||
<CountDownInput
|
||||
disabled={!this.state.validEmail}
|
||||
onButtonClickArgs={[this.state.email, "email", Setting.getApplicationOrgName(application)]}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
} else if (signupItem.name === "Phone") {
|
||||
|
@ -69,3 +69,10 @@ export function deleteApplication(application) {
|
||||
body: JSON.stringify(newApplication),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getSamlMetadata(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/saml/metadata?application=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
}).then(res => res.text());
|
||||
}
|
||||
|
56
web/src/backend/ModelBackend.js
Normal file
56
web/src/backend/ModelBackend.js
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as Setting from "../Setting";
|
||||
|
||||
export function getModels(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-models?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getModel(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-model?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateModel(owner, name, model) {
|
||||
let newModel = Setting.deepCopy(model);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-model?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(newModel),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addModel(model) {
|
||||
let newModel = Setting.deepCopy(model);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-model`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(newModel),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteModel(model) {
|
||||
let newModel = Setting.deepCopy(model);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-model`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(newModel),
|
||||
}).then(res => res.json());
|
||||
}
|
@ -54,3 +54,10 @@ export function deletePayment(payment) {
|
||||
body: JSON.stringify(newPayment),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function invoicePayment(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/invoice-payment?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include"
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -54,3 +54,10 @@ export function deleteSyncer(syncer) {
|
||||
body: JSON.stringify(newSyncer),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function runSyncer(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/run-syncer?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {Card, Col, Row} from "antd";
|
||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||
import * as Setting from "../Setting";
|
||||
import SingleCard from "./SingleCard";
|
||||
import i18next from "i18next";
|
||||
@ -23,9 +24,23 @@ class HomePage extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
applications: null,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getApplicationsByOrganization(this.props.account.owner);
|
||||
}
|
||||
|
||||
getApplicationsByOrganization(organizationName) {
|
||||
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
applications: (res.msg === undefined) ? res : [],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getItems() {
|
||||
let items = [];
|
||||
if (Setting.isAdminUser(this.props.account)) {
|
||||
@ -35,25 +50,32 @@ class HomePage extends React.Component {
|
||||
{link: "/providers", name: i18next.t("general:Providers"), organizer: i18next.t("general:OAuth providers")},
|
||||
{link: "/applications", name: i18next.t("general:Applications"), organizer: i18next.t("general:Applications that require authentication")},
|
||||
];
|
||||
} else {
|
||||
items = [
|
||||
{link: "/account", name: i18next.t("account:My Account"), organizer: i18next.t("account:Settings for your account")},
|
||||
];
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i ++) {
|
||||
let filename = items[i].link;
|
||||
if (filename === "/account") {
|
||||
filename = "/users";
|
||||
for (let i = 0; i < items.length; i ++) {
|
||||
let filename = items[i].link;
|
||||
if (filename === "/account") {
|
||||
filename = "/users";
|
||||
}
|
||||
items[i].logo = `https://cdn.casbin.com/static/img${filename}.png`;
|
||||
items[i].createdTime = "";
|
||||
}
|
||||
items[i].logo = `https://cdn.casbin.com/static/img${filename}.png`;
|
||||
items[i].createdTime = "";
|
||||
} else {
|
||||
this.state.applications.forEach(application => {
|
||||
console.log(application)
|
||||
items.push({
|
||||
link: application.homepageUrl, name: application.displayName, organizer: application.description, logo: application.logo, createdTime: "",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
renderCards() {
|
||||
if (this.state.applications === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = this.getItems();
|
||||
|
||||
if (Setting.isMobile()) {
|
||||
|
@ -51,10 +51,10 @@ class SingleCard extends React.Component {
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
<img alt="logo" src={logo} width={"100%"} height={"100%"} />
|
||||
<img alt="logo" src={logo} style={{width: "100%", height: "210px", objectFit: "scale-down"}} />
|
||||
}
|
||||
onClick={() => Setting.goToLinkSoft(this, link)}
|
||||
style={isSingle ? {width: "320px"} : null}
|
||||
style={isSingle ? {width: "320px"} : {width: "100%"}}
|
||||
>
|
||||
<Meta title={title} description={desc} />
|
||||
<br/>
|
||||
|
@ -18,8 +18,6 @@ import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
import * as UserBackend from "../backend/UserBackend";
|
||||
import {SafetyOutlined} from "@ant-design/icons";
|
||||
import * as Util from "../auth/Util";
|
||||
import {isValidEmail, isValidPhone} from "../Setting";
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
|
@ -142,7 +142,7 @@ class OAuthWidget extends React.Component {
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={24 - this.props.labelSpan} >
|
||||
<img style={{marginRight: '10px'}} width={30} height={30} src={avatarUrl} alt={name} />
|
||||
<img style={{marginRight: '10px'}} width={30} height={30} src={avatarUrl} alt={name} referrerPolicy="no-referrer" />
|
||||
<span style={{width: this.props.labelSpan === 3 ? '300px' : '130px', display: (Setting.isMobile()) ? 'inline' : "inline-block"}}>
|
||||
{
|
||||
linkedValue === "" ? (
|
||||
|
@ -3,7 +3,6 @@
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"My Account": "Mein Konto",
|
||||
"Settings for your account": "Einstellungen für Ihr Konto",
|
||||
"Sign Up": "Registrieren"
|
||||
},
|
||||
"application": {
|
||||
@ -171,9 +170,11 @@
|
||||
"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.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sync",
|
||||
"Syncers": "Syncers",
|
||||
"Timestamp": "Zeitstempel",
|
||||
"Tokens": "Token",
|
||||
@ -223,7 +224,7 @@
|
||||
"Continue with": "Weiter mit",
|
||||
"Email or phone": "E-Mail oder Telefon",
|
||||
"Forgot password?": "Passwort vergessen?",
|
||||
"Invalid Email or phone": "Ungültige E-Mail oder Telefon",
|
||||
"Logging out...": "Logging out...",
|
||||
"No account?": "Kein Konto?",
|
||||
"Or sign in with another account": "Oder melden Sie sich mit einem anderen Konto an",
|
||||
"Password": "Passwort",
|
||||
@ -246,6 +247,8 @@
|
||||
"Default avatar": "Standard Avatar",
|
||||
"Edit Organization": "Organisation bearbeiten",
|
||||
"Favicon": "Févicon",
|
||||
"Is profile public": "Is profile public",
|
||||
"Is profile public - Tooltip": "Is profile public - Tooltip",
|
||||
"New Organization": "New Organization",
|
||||
"Soft deletion": "Weiche Löschung",
|
||||
"Soft deletion - Tooltip": "Weiche Löschung - Tooltip",
|
||||
@ -255,11 +258,40 @@
|
||||
"Website URL - Tooltip": "Unique string-style identifier"
|
||||
},
|
||||
"payment": {
|
||||
"Confirm your invoice information": "Confirm your invoice information",
|
||||
"Currency": "Currency",
|
||||
"Currency - Tooltip": "Currency - Tooltip",
|
||||
"Download Invoice": "Download Invoice",
|
||||
"Edit Payment": "Edit Payment",
|
||||
"Individual": "Individual",
|
||||
"Invoice URL": "Invoice URL",
|
||||
"Invoice URL - Tooltip": "Invoice URL - Tooltip",
|
||||
"Invoice actions": "Invoice actions",
|
||||
"Invoice actions - Tooltip": "Invoice actions - Tooltip",
|
||||
"Invoice remark": "Invoice remark",
|
||||
"Invoice remark - Tooltip": "Invoice remark - Tooltip",
|
||||
"Invoice tax ID": "Invoice tax ID",
|
||||
"Invoice tax ID - Tooltip": "Invoice tax ID - Tooltip",
|
||||
"Invoice title": "Invoice title",
|
||||
"Invoice title - Tooltip": "Invoice title - Tooltip",
|
||||
"Invoice type": "Invoice type",
|
||||
"Invoice type - Tooltip": "Invoice type - Tooltip",
|
||||
"Issue Invoice": "Issue Invoice",
|
||||
"Message": "Message",
|
||||
"Message - Tooltip": "Message - Tooltip",
|
||||
"New Payment": "New Payment",
|
||||
"Organization": "Organization",
|
||||
"Person Email": "Person Email",
|
||||
"Person Email - Tooltip": "Person Email - Tooltip",
|
||||
"Person ID card": "Person ID card",
|
||||
"Person ID card - Tooltip": "Person ID card - Tooltip",
|
||||
"Person name": "Person name",
|
||||
"Person name - Tooltip": "Person name - Tooltip",
|
||||
"Person phone": "Person phone",
|
||||
"Person phone - Tooltip": "Person phone - Tooltip",
|
||||
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.",
|
||||
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
|
||||
"Please pay the order first!": "Please pay the order first!",
|
||||
"Price": "Price",
|
||||
"Price - Tooltip": "Price - Tooltip",
|
||||
"Processing...": "Processing...",
|
||||
@ -330,11 +362,19 @@
|
||||
"Agent ID - Tooltip": "Agent ID - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key - Tooltip",
|
||||
"App secret": "App secret",
|
||||
"AppSecret - Tooltip": "AppSecret - Tooltip",
|
||||
"Auth URL": "Auth URL",
|
||||
"Auth URL - Tooltip": "Auth URL - Tooltip",
|
||||
"Bucket": "Eimer",
|
||||
"Bucket - Tooltip": "Storage bucket name",
|
||||
"Can not parse Metadata": "Metadaten können nicht analysiert werden",
|
||||
"Category": "Kategorie",
|
||||
"Category - Tooltip": "Unique string-style identifier",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No. - Tooltip",
|
||||
"Client ID": "Kunden-ID",
|
||||
"Client ID - Tooltip": "Unique string-style identifier",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@ -382,6 +422,8 @@
|
||||
"SP ACS URL": "SP-ACS-URL",
|
||||
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
|
||||
"SP Entity ID": "SP Entity ID",
|
||||
"Scope": "Scope",
|
||||
"Scope - Tooltip": "Scope - Tooltip",
|
||||
"Secret access key": "Geheimer Zugangsschlüssel",
|
||||
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
|
||||
"Sign Name": "Schild Name",
|
||||
@ -400,8 +442,12 @@
|
||||
"Template Code - Tooltip": "Unique string-style identifier",
|
||||
"Terms of Use": "Nutzungsbedingungen",
|
||||
"Terms of Use - Tooltip": "Nutzungsbedingungen - Tooltip",
|
||||
"Token URL": "Token URL",
|
||||
"Token URL - Tooltip": "Token URL - Tooltip",
|
||||
"Type": "Typ",
|
||||
"Type - Tooltip": "Unique string-style identifier",
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"alertType": "alarmtyp",
|
||||
"canSignIn": "canSignIn",
|
||||
"canSignUp": "canSignUp",
|
||||
@ -455,6 +501,8 @@
|
||||
"Please input your real name!": "Bitte geben Sie Ihren persönlichen Namen ein!",
|
||||
"Please select your country/region!": "Bitte wählen Sie Ihr Land/Ihre Region!",
|
||||
"Terms of Use": "Nutzungsbedingungen",
|
||||
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
|
||||
"The input is not invoice title!": "The input is not invoice title!",
|
||||
"The input is not valid Email!": "Die Eingabe ist ungültig!",
|
||||
"The input is not valid Phone!": "Die Eingabe ist nicht gültig!",
|
||||
"Unknown Check Type": "Unbekannter Schecktyp",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
"My Account": "My Account",
|
||||
"Settings for your account": "Settings for your account",
|
||||
"Sign Up": "Sign Up"
|
||||
},
|
||||
"application": {
|
||||
@ -171,9 +170,11 @@
|
||||
"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.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sync",
|
||||
"Syncers": "Syncers",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
@ -223,7 +224,7 @@
|
||||
"Continue with": "Continue with",
|
||||
"Email or phone": "Email or phone",
|
||||
"Forgot password?": "Forgot password?",
|
||||
"Invalid Email or phone": "Invalid Email or phone",
|
||||
"Logging out...": "Logging out...",
|
||||
"No account?": "No account?",
|
||||
"Or sign in with another account": "Or sign in with another account",
|
||||
"Password": "Password",
|
||||
@ -246,6 +247,8 @@
|
||||
"Default avatar": "Default avatar",
|
||||
"Edit Organization": "Edit Organization",
|
||||
"Favicon": "Favicon",
|
||||
"Is profile public": "Is profile public",
|
||||
"Is profile public - Tooltip": "Is profile public - Tooltip",
|
||||
"New Organization": "New Organization",
|
||||
"Soft deletion": "Soft deletion",
|
||||
"Soft deletion - Tooltip": "Soft deletion - Tooltip",
|
||||
@ -255,11 +258,40 @@
|
||||
"Website URL - Tooltip": "Website URL - Tooltip"
|
||||
},
|
||||
"payment": {
|
||||
"Confirm your invoice information": "Confirm your invoice information",
|
||||
"Currency": "Currency",
|
||||
"Currency - Tooltip": "Currency - Tooltip",
|
||||
"Download Invoice": "Download Invoice",
|
||||
"Edit Payment": "Edit Payment",
|
||||
"Individual": "Individual",
|
||||
"Invoice URL": "Invoice URL",
|
||||
"Invoice URL - Tooltip": "Invoice URL - Tooltip",
|
||||
"Invoice actions": "Invoice actions",
|
||||
"Invoice actions - Tooltip": "Invoice actions - Tooltip",
|
||||
"Invoice remark": "Invoice remark",
|
||||
"Invoice remark - Tooltip": "Invoice remark - Tooltip",
|
||||
"Invoice tax ID": "Invoice tax ID",
|
||||
"Invoice tax ID - Tooltip": "Invoice tax ID - Tooltip",
|
||||
"Invoice title": "Invoice title",
|
||||
"Invoice title - Tooltip": "Invoice title - Tooltip",
|
||||
"Invoice type": "Invoice type",
|
||||
"Invoice type - Tooltip": "Invoice type - Tooltip",
|
||||
"Issue Invoice": "Issue Invoice",
|
||||
"Message": "Message",
|
||||
"Message - Tooltip": "Message - Tooltip",
|
||||
"New Payment": "New Payment",
|
||||
"Organization": "Organization",
|
||||
"Person Email": "Person Email",
|
||||
"Person Email - Tooltip": "Person Email - Tooltip",
|
||||
"Person ID card": "Person ID card",
|
||||
"Person ID card - Tooltip": "Person ID card - Tooltip",
|
||||
"Person name": "Person name",
|
||||
"Person name - Tooltip": "Person name - Tooltip",
|
||||
"Person phone": "Person phone",
|
||||
"Person phone - Tooltip": "Person phone - Tooltip",
|
||||
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.",
|
||||
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
|
||||
"Please pay the order first!": "Please pay the order first!",
|
||||
"Price": "Price",
|
||||
"Price - Tooltip": "Price - Tooltip",
|
||||
"Processing...": "Processing...",
|
||||
@ -330,11 +362,19 @@
|
||||
"Agent ID - Tooltip": "Agent ID - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key - Tooltip",
|
||||
"App secret": "App secret",
|
||||
"AppSecret - Tooltip": "AppSecret - Tooltip",
|
||||
"Auth URL": "Auth URL",
|
||||
"Auth URL - Tooltip": "Auth URL - Tooltip",
|
||||
"Bucket": "Bucket",
|
||||
"Bucket - Tooltip": "Bucket - Tooltip",
|
||||
"Can not parse Metadata": "Can not parse Metadata",
|
||||
"Category": "Category",
|
||||
"Category - Tooltip": "Category - Tooltip",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No. - Tooltip",
|
||||
"Client ID": "Client ID",
|
||||
"Client ID - Tooltip": "Client ID - Tooltip",
|
||||
"Client ID 2": "Client ID 2",
|
||||
@ -382,6 +422,8 @@
|
||||
"SP ACS URL": "SP ACS URL",
|
||||
"SP ACS URL - Tooltip": "SP ACS URL - Tooltip",
|
||||
"SP Entity ID": "SP Entity ID",
|
||||
"Scope": "Scope",
|
||||
"Scope - Tooltip": "Scope - Tooltip",
|
||||
"Secret access key": "Secret access key",
|
||||
"SecretAccessKey - Tooltip": "SecretAccessKey - Tooltip",
|
||||
"Sign Name": "Sign Name",
|
||||
@ -400,8 +442,12 @@
|
||||
"Template Code - Tooltip": "Template Code - Tooltip",
|
||||
"Terms of Use": "Terms of Use",
|
||||
"Terms of Use - Tooltip": "Terms of Use - Tooltip",
|
||||
"Token URL": "Token URL",
|
||||
"Token URL - Tooltip": "Token URL - Tooltip",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"alertType": "alertType",
|
||||
"canSignIn": "canSignIn",
|
||||
"canSignUp": "canSignUp",
|
||||
@ -455,6 +501,8 @@
|
||||
"Please input your real name!": "Please input your real name!",
|
||||
"Please select your country/region!": "Please select your country/region!",
|
||||
"Terms of Use": "Terms of Use",
|
||||
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
|
||||
"The input is not invoice title!": "The input is not invoice title!",
|
||||
"The input is not valid Email!": "The input is not valid Email!",
|
||||
"The input is not valid Phone!": "The input is not valid Phone!",
|
||||
"Unknown Check Type": "Unknown Check Type",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"Login": "Se connecter",
|
||||
"Logout": "Déconnexion",
|
||||
"My Account": "Mon Compte",
|
||||
"Settings for your account": "Paramètres de votre compte",
|
||||
"Sign Up": "S'inscrire"
|
||||
},
|
||||
"application": {
|
||||
@ -171,9 +170,11 @@
|
||||
"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.",
|
||||
"Sorry, the user you visited does not exist or you are not authorized to access this user.": "Sorry, the user you visited does not exist or you are not authorized to access this user.",
|
||||
"State": "State",
|
||||
"State - Tooltip": "State - Tooltip",
|
||||
"Swagger": "Swagger",
|
||||
"Sync": "Sync",
|
||||
"Syncers": "Synchronisateurs",
|
||||
"Timestamp": "Horodatage",
|
||||
"Tokens": "Jetons",
|
||||
@ -223,7 +224,7 @@
|
||||
"Continue with": "Continuer avec",
|
||||
"Email or phone": "Courriel ou téléphone",
|
||||
"Forgot password?": "Mot de passe oublié ?",
|
||||
"Invalid Email or phone": "E-mail ou téléphone invalide",
|
||||
"Logging out...": "Logging out...",
|
||||
"No account?": "Pas de compte ?",
|
||||
"Or sign in with another account": "Ou connectez-vous avec un autre compte",
|
||||
"Password": "Mot de passe",
|
||||
@ -246,6 +247,8 @@
|
||||
"Default avatar": "Avatar par défaut",
|
||||
"Edit Organization": "Modifier l'organisation",
|
||||
"Favicon": "Favicon",
|
||||
"Is profile public": "Is profile public",
|
||||
"Is profile public - Tooltip": "Is profile public - Tooltip",
|
||||
"New Organization": "New Organization",
|
||||
"Soft deletion": "Suppression du logiciel",
|
||||
"Soft deletion - Tooltip": "Suppression de soft - infobulle",
|
||||
@ -255,11 +258,40 @@
|
||||
"Website URL - Tooltip": "Unique string-style identifier"
|
||||
},
|
||||
"payment": {
|
||||
"Confirm your invoice information": "Confirm your invoice information",
|
||||
"Currency": "Currency",
|
||||
"Currency - Tooltip": "Currency - Tooltip",
|
||||
"Download Invoice": "Download Invoice",
|
||||
"Edit Payment": "Edit Payment",
|
||||
"Individual": "Individual",
|
||||
"Invoice URL": "Invoice URL",
|
||||
"Invoice URL - Tooltip": "Invoice URL - Tooltip",
|
||||
"Invoice actions": "Invoice actions",
|
||||
"Invoice actions - Tooltip": "Invoice actions - Tooltip",
|
||||
"Invoice remark": "Invoice remark",
|
||||
"Invoice remark - Tooltip": "Invoice remark - Tooltip",
|
||||
"Invoice tax ID": "Invoice tax ID",
|
||||
"Invoice tax ID - Tooltip": "Invoice tax ID - Tooltip",
|
||||
"Invoice title": "Invoice title",
|
||||
"Invoice title - Tooltip": "Invoice title - Tooltip",
|
||||
"Invoice type": "Invoice type",
|
||||
"Invoice type - Tooltip": "Invoice type - Tooltip",
|
||||
"Issue Invoice": "Issue Invoice",
|
||||
"Message": "Message",
|
||||
"Message - Tooltip": "Message - Tooltip",
|
||||
"New Payment": "New Payment",
|
||||
"Organization": "Organization",
|
||||
"Person Email": "Person Email",
|
||||
"Person Email - Tooltip": "Person Email - Tooltip",
|
||||
"Person ID card": "Person ID card",
|
||||
"Person ID card - Tooltip": "Person ID card - Tooltip",
|
||||
"Person name": "Person name",
|
||||
"Person name - Tooltip": "Person name - Tooltip",
|
||||
"Person phone": "Person phone",
|
||||
"Person phone - Tooltip": "Person phone - Tooltip",
|
||||
"Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.": "Please carefully check your invoice information. Once the invoice is issued, it cannot be withdrawn or modified.",
|
||||
"Please click the below button to return to the original website": "Please click the below button to return to the original website",
|
||||
"Please pay the order first!": "Please pay the order first!",
|
||||
"Price": "Price",
|
||||
"Price - Tooltip": "Price - Tooltip",
|
||||
"Processing...": "Processing...",
|
||||
@ -330,11 +362,19 @@
|
||||
"Agent ID - Tooltip": "Agent ID - Tooltip",
|
||||
"App ID": "App ID",
|
||||
"App ID - Tooltip": "App ID - Tooltip",
|
||||
"App key": "App key",
|
||||
"App key - Tooltip": "App key - Tooltip",
|
||||
"App secret": "App secret",
|
||||
"AppSecret - Tooltip": "AppSecret - Tooltip",
|
||||
"Auth URL": "Auth URL",
|
||||
"Auth URL - Tooltip": "Auth URL - Tooltip",
|
||||
"Bucket": "Seau",
|
||||
"Bucket - Tooltip": "Storage bucket name",
|
||||
"Can not parse Metadata": "Impossible d'analyser les métadonnées",
|
||||
"Category": "Catégorie",
|
||||
"Category - Tooltip": "Unique string-style identifier",
|
||||
"Channel No.": "Channel No.",
|
||||
"Channel No. - Tooltip": "Channel No. - Tooltip",
|
||||
"Client ID": "ID du client",
|
||||
"Client ID - Tooltip": "Unique string-style identifier",
|
||||
"Client ID 2": "ID client 2",
|
||||
@ -382,6 +422,8 @@
|
||||
"SP ACS URL": "URL du SP ACS",
|
||||
"SP ACS URL - Tooltip": "URL SP ACS - infobulle",
|
||||
"SP Entity ID": "ID de l'entité SP",
|
||||
"Scope": "Scope",
|
||||
"Scope - Tooltip": "Scope - Tooltip",
|
||||
"Secret access key": "Clé d'accès secrète",
|
||||
"SecretAccessKey - Tooltip": "SecretAccessKey - Infobulle",
|
||||
"Sign Name": "Nom du panneau",
|
||||
@ -400,8 +442,12 @@
|
||||
"Template Code - Tooltip": "Unique string-style identifier",
|
||||
"Terms of Use": "Conditions d'utilisation",
|
||||
"Terms of Use - Tooltip": "Conditions d'utilisation - Info-bulle",
|
||||
"Token URL": "Token URL",
|
||||
"Token URL - Tooltip": "Token URL - Tooltip",
|
||||
"Type": "Type de texte",
|
||||
"Type - Tooltip": "Unique string-style identifier",
|
||||
"UserInfo URL": "UserInfo URL",
|
||||
"UserInfo URL - Tooltip": "UserInfo URL - Tooltip",
|
||||
"alertType": "Type d'alerte",
|
||||
"canSignIn": "canSignIn",
|
||||
"canSignUp": "canSignUp",
|
||||
@ -455,6 +501,8 @@
|
||||
"Please input your real name!": "Veuillez entrer votre nom personnel !",
|
||||
"Please select your country/region!": "Veuillez sélectionner votre pays/région!",
|
||||
"Terms of Use": "Conditions d'utilisation",
|
||||
"The input is not invoice Tax ID!": "The input is not invoice Tax ID!",
|
||||
"The input is not invoice title!": "The input is not invoice title!",
|
||||
"The input is not valid Email!": "L'entrée n'est pas un email valide !",
|
||||
"The input is not valid Phone!": "L'entrée n'est pas un téléphone valide !",
|
||||
"Unknown Check Type": "Type de vérification inconnu",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user