Compare commits

...

82 Commits

Author SHA1 Message Date
leoshine
376bac15dc fix: improve swagger Api docunment (#812) 2022-06-21 23:11:29 +08:00
Gucheng Wang
8d0e92edef Fix missing items in renderAccountItem(). 2022-06-21 17:08:08 +08:00
Gucheng Wang
0075b7af52 Fix JS warnings. 2022-06-21 15:26:58 +08:00
Resulte Lee
2c57bece39 feat: fix stuck error when no captcha provider found (#808) 2022-06-21 12:22:46 +08:00
Resulte Lee
2e42511bc4 feat: support configurable captcha(reCaptcha & hCaptcha) (#765)
* feat: support configurable captcha(layered architecture)

* refactor & add captcha logo

* rename captcha

* Update authz.go

* Update hcaptcha.go

* Update default.go

* Update recaptcha.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-18 16:00:31 +08:00
Gucheng Wang
ae4ab9902b Add accountTable. 2022-06-18 01:41:21 +08:00
Gucheng Wang
065b235dc5 Fix signupTable i18n. 2022-06-17 23:26:02 +08:00
Yixiang Zhao
63c09a879f fix: disable jsx-a11y/anchor-is-valid (#800)
* fix: disable jsx-a11y/anchor-is-valid

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

* Update LoginPage.js

* Update SignupPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-17 19:57:11 +08:00
limix
61c80e790f Fix Authentication failure! invalid_ticket: OneLogin::RubySaml::ValidationError #798 (#799) 2022-06-17 18:35:44 +08:00
Gucheng Wang
be91ff47aa Fix logo columns. 2022-06-17 00:07:16 +08:00
Gucheng Wang
b4c18eb7a4 Use codemirror for samlMetadata. 2022-06-16 23:59:18 +08:00
Gucheng Wang
0f483fb65b Improve preview buttons to copy link. 2022-06-16 22:01:09 +08:00
Gucheng Wang
ebe9889d58 Improve i18n 2022-06-16 21:35:52 +08:00
Gucheng Wang
ee42fcac8e Remove signup_item.go 2022-06-16 20:52:54 +08:00
ziliangyu
6187b48f61 fix: show alert when user clicks on application edit page's preview window (#794)
* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* Update ApplicationEditPage.js

* fix: show alert when user clicks on application edit page's preview window

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-15 22:11:37 +08:00
Gucheng Wang
2020955270 Fix cannot support old Docker version bug, revert PR: https://github.com/casdoor/casdoor/pull/606 2022-06-15 01:20:00 +08:00
Gucheng Wang
1b5a8f8e57 Fix missing i18n text. 2022-06-15 00:55:06 +08:00
Товарищ программист
ff94e5164a feat: fix incorrect CAS url concatenation (#795)
* fix: fix incorrect cas url concatenation

* Update LoginPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-14 21:51:40 +08:00
ziliangyu
15a6fd2b52 feat: show alert when user clicks on application edit page's preview wi… (#791)
* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* fix:Show alert when user clicks on application edit page's preview window

* fix: Show alert when user clicks on application edit page's preview window in preview

* Update ApplicationEditPage.js

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-13 12:18:18 +08:00
Ryao
37b6b50751 fix: remove redundant query for OAuth user (#788) 2022-06-10 15:58:22 +08:00
Ryao
efe5431f54 fix: OAuth user id confusion caused by username (#785) 2022-06-10 00:08:26 +08:00
Yixiang Zhao
e9159902eb fix: fix the web compiled warnings (#778)
* fix: fix the web compiled warnings

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

* fix: disable changeMomentLanguage

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

* Update SyncerEditPage.js

* Update UserEditPage.js

* Update ResourceListPage.js

* Update ProviderEditPage.js

* Update ProductBuyPage.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-06-05 20:56:31 +08:00
caoshengdong
604e2757c8 fix: fix the problem that user owner is not updated when updating organization name (#775)
* fix: use openid or unionid as username rather than nickname when logging with WeChat
FIX #762

* fix: fix the problem that user owner is not updated when updating organization name

* Update wechat.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-03 00:37:22 +08:00
Gucheng Wang
88c5aae9e9 Fix meta desc info. 2022-06-01 22:22:00 +08:00
Товарищ программист
3d0cf8788b fix: trigger missing webhook (#770)
* fix: trigger missing webhook

* Update auth.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-06-01 09:34:56 +08:00
Yixiang Zhao
e78ea2546f fix: bilibili name and avatar (#772)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-31 21:54:00 +08:00
Roobtyan
f7705931f7 fix: handle WeChat username conflicts (#771)
* handle username conflicts

* Update auth.go

Co-authored-by: roobtyan <roobtyan@qq.com>
Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-05-31 21:51:41 +08:00
caoshengdong
5d8b710bf7 fix: use openid or unionid as username rather than nickname when logging with WeChat (#763)
FIX #762
2022-05-31 21:22:10 +08:00
Yixiang Zhao
b85ad896bf fix: saml endpoint crash (#773)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-31 21:10:35 +08:00
Yixiang Zhao
42c2210178 fix: set phone prefix when disable verification code (#769)
Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-30 18:26:42 +08:00
Yixiang Zhao
d52caed3a9 feat: add model page (#757)
* feat: add model page

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

* feat: support config model for permission

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

* translation and indentation

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-24 18:27:47 +08:00
Gucheng Wang
27d8cd758d Simplify README 2022-05-23 21:45:31 +08:00
greenhandatsjtu
98f77960de feat: add Douyin OAuth provider (#753) 2022-05-15 20:59:21 +08:00
Yixiang Zhao
e5b71a08ae feat: support "+" in syncer column name (#752)
* feat: support + in syncer column name

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

* feat: trim

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-05-13 20:24:46 +08:00
Jiakuan Li
3ad4b7a43c feat: add Bilibili OAuth (#720)
* implemented bilibili oauth

* add bilibili oauth

* add document address

* add frontend page

* uncheck
2022-05-12 10:07:52 +08:00
Yixiang Zhao
c5c3a08aa9 feat: add saml metadata in application edit page (#750)
* feat: add saml metadata in application edit page

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

* Update ApplicationEditPage.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-05-11 20:23:36 +08:00
greenhandatsjtu
8efd964835 fix: unchanged masked client_secret/password updated to *** (#749) 2022-05-10 17:37:12 +08:00
Товарищ программист
5dac87a4c3 feat: hide proxy-test output (#746)
* feat: hide proxy-test output

* Update build.sh

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-05-07 20:56:12 +08:00
Gucheng Wang
49c3266400 Fix missing OIDC response_types_supported. 2022-05-07 09:36:20 +08:00
Gucheng Wang
39548d5d72 Change cert default algorithm to RS256. 2022-05-06 09:34:42 +08:00
Gucheng Wang
1c949e415e Add refresh_token to app grantTypes. 2022-05-06 09:31:22 +08:00
Jan Piechowicz
1b840a2e9f feat: support argon2id pass manager (#744)
* support for argon2id

* Update argon2id.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-05-06 09:25:42 +08:00
Gucheng Wang
c9849d8b55 Accept more file formats in upload. 2022-05-04 23:16:24 +08:00
xiexianbin
b747f5e27c fix: mistake GetApplicationLogin swagger router (casdoor#739) (#740)
Signed-off-by: xiexianbin <me@xiexianbin.cn>
2022-05-04 19:15:13 +08:00
一路向北
8b340105c1 fix: fix missing OpenLDAP uid in ldap.go 2022-05-04 10:23:17 +08:00
Resulte Lee
43b1006f11 fix: sign up without email verification do not work (#736) 2022-05-03 18:05:58 +08:00
leoshine
78efc9c2d0 feat: add azure storage support (#735)
* feat: add zure storage support

* Update local_file_system.go

Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
2022-05-03 17:59:07 +08:00
Resulte Lee
c4089eacb7 feat: Allow to sign up with Email without verification (#728)
* feat: Allow to sign up with Email without verification by rule

* Update account.go

* Update SignupTable.js

* Update SignupPage.js

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-05-02 17:19:40 +08:00
greenhandatsjtu
4acba2d493 Add entrypoint to docker-compose casdoor service (#727) 2022-05-01 19:26:31 +08:00
greenhandatsjtu
fc0ca4cceb Add Okta OAuth provider (#729) 2022-05-01 18:31:42 +08:00
Resulte Lee
912d9d0c01 feat: DingTalk provider value case unsensitive (#724) 2022-04-30 16:20:20 +08:00
zc
8e48bddf5f remove extra parentheses showing account numbers (#726) 2022-04-30 15:20:08 +08:00
Yixiang Zhao
c05fb77224 fix: set sync ldap user default attributes (#721)
* fix: set the password of the sync ldap user to empty

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

* fix: set sync ldap user default attributes

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-04-29 21:44:13 +08:00
Gucheng Wang
9af9ead939 Return invoiceUrl in invoice-payment API. 2022-04-28 15:07:57 +08:00
Gucheng Wang
f5590c42f7 Add payerName to provider. 2022-04-28 14:50:59 +08:00
Gucheng Wang
5597f99e3c Scroll to payment page bottom. 2022-04-27 01:32:36 +08:00
Gucheng Wang
ea005aaf4d Improve InvoicePayment() error handling. 2022-04-27 00:24:48 +08:00
Gucheng Wang
e5c1f560c5 Fix bug in payment. 2022-04-27 00:07:13 +08:00
Gucheng Wang
20fc7d1b58 Add payment modal. 2022-04-26 23:40:33 +08:00
Gucheng Wang
cf3b46130b Add InvoicePayment() API. 2022-04-26 22:17:53 +08:00
halozhy
cab51fae9c fix: add 'use' and 'alg' in .well-known/jwks (#708)
* fix: add 'use' and 'alg' in .well-known/jwks

* fix: dynamically assign value to 'alg' param
2022-04-26 21:53:05 +08:00
Frank Chang
b867872da4 fix: return right after error response on GetUserInfo (#707) 2022-04-26 14:32:04 +08:00
Gucheng Wang
305867f49a Add checkError() to payment. 2022-04-25 21:39:46 +08:00
Gucheng Wang
3f90c18a19 Add invoiceType to payment. 2022-04-25 20:58:53 +08:00
Gucheng Wang
9e5a64c021 Add new payment fields 2022-04-25 20:40:50 +08:00
Gucheng Wang
4263af6f2c Fix frontend warnings. 2022-04-25 20:00:57 +08:00
Gucheng Wang
3e92d761b9 Fix i18n translations. 2022-04-25 19:46:45 +08:00
Gucheng Wang
0e41568f62 Add apps to homepage. 2022-04-25 13:51:46 +08:00
Kevin Fu
fb7e2729c6 fix: support Microsoft AD user search (#704) 2022-04-25 12:20:59 +08:00
akkuman
28b9154d7e fix: fix #693 token error (#695) 2022-04-23 01:12:06 +08:00
Товарищ программист
b0b3eb0805 fix: fix failure of introspection (#682)
* fix: fix failure of introspection

* Update token.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-04-22 22:45:52 +08:00
niko7g
73bd9dd517 bugfix #664 Casdoor fails to start when there is already a database (#681)
Signed-off-by: niko7g <niko7.g@gmail.com>
2022-04-22 22:17:03 +08:00
akkuman
0bc8c2d15f fix: recover when goroutine panic that will kill main program (#692)
* fix #684

recover when goroutine panic that will kill main program

* Update util.go

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2022-04-22 21:59:06 +08:00
akkuman
7b78e60265 fix: close the resp in time (#689) 2022-04-21 23:22:50 +08:00
akkuman
7464f9a8ad fix: when req error, read body(nil) will panic (#690) 2022-04-21 22:14:01 +08:00
akkuman
d3a7a062d3 fix #687 (#688)
fix the display bug on the personal binding information page
2022-04-21 21:52:34 +08:00
Yixiang Zhao
67a0264411 feat: add sync button to execute syncer once (#668)
* feat: add sync button to execute syncer once

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

* fix: requested changes

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

* fix: requested changes

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-04-18 16:27:34 +08:00
疯魔慕薇
a6a055cc83 Fix: ExpiresIn of token should be seconds. (#676)
Signed-off-by: 疯魔慕薇 <kfanjian@gmail.com>
2022-04-18 10:57:51 +08:00
Dean CN
a89a7f9eb7 bug fix (#674) 2022-04-17 17:01:56 +08:00
halozhy
287f60353c feat: try to support custom OAuth provider (#667)
* feat: try to support private provider

* fix: modify code according to code review

* feat: set example values for custom params
2022-04-16 17:17:45 +08:00
Yixiang Zhao
530330bd66 feat: add isProfilePublic setting for accessing user info (#656)
* feat: add isProfilePublic setting for accessing user info

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

* fix: requested changes

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
2022-04-16 15:10:03 +08:00
Yang Luo
70a1428972 Improve resource DB column length. 2022-04-16 13:23:05 +08:00
126 changed files with 6153 additions and 1035 deletions

0
$env Normal file
View File

152
README.md
View File

@@ -42,166 +42,66 @@
</a> </a>
</p> </p>
## Online demo ## 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 - International: https://casdoor.org
go get github.com/casdoor/casdoor - Asian mirror: https://docs.casdoor.cn
```
or `git`:
```bash
git clone https://github.com/casdoor/casdoor
```
Finally, change directory: ## Install
```bash - By source code: https://casdoor.org/docs/basic/server-installation
cd casdoor/ - By Docker: https://casdoor.org/docs/basic/try-with-docker
```
We provide two start up methods for all kinds of users.
### Manual
#### Simple configuration ## How to connect to Casdoor?
Casdoor requires a running Relational database to be operational.Thus you need to modify configuration to point out the location of database.
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 - Docs: https://casdoor.org/docs/basic/public-api
createDatabase := flag.Bool("createDatabase", false, "true if you need casdoor to create database") - 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 - Gitter: https://gitter.im/casbin/casdoor
go run main.go - 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 ## 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). 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. 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 ## License
[Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE) [Apache-2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE)

View File

@@ -80,21 +80,23 @@ p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, * p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/get-account, *, * p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, * p, *, *, GET, /api/userinfo, *, *
p, *, *, POST, /api/login/oauth/access_token, *, * p, *, *, *, /api/login/oauth, *, *
p, *, *, POST, /api/login/oauth/refresh_token, *, *
p, *, *, GET, /api/login/oauth/logout, *, *
p, *, *, GET, /api/get-application, *, * p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-applications, *, *
p, *, *, GET, /api/get-user, *, * p, *, *, GET, /api/get-user, *, *
p, *, *, GET, /api/get-user-application, *, * p, *, *, GET, /api/get-user-application, *, *
p, *, *, GET, /api/get-resources, *, * p, *, *, GET, /api/get-resources, *, *
p, *, *, GET, /api/get-product, *, * p, *, *, GET, /api/get-product, *, *
p, *, *, POST, /api/buy-product, *, * p, *, *, POST, /api/buy-product, *, *
p, *, *, GET, /api/get-payment, *, * p, *, *, GET, /api/get-payment, *, *
p, *, *, POST, /api/update-payment, *, *
p, *, *, POST, /api/invoice-payment, *, *
p, *, *, GET, /api/get-providers, *, * p, *, *, GET, /api/get-providers, *, *
p, *, *, POST, /api/unlink, *, * p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, * p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, * p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-human-check, *, * p, *, *, GET, /api/get-captcha, *, *
p, *, *, POST, /api/verify-captcha, *, *
p, *, *, POST, /api/reset-email-or-phone, *, * p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, * p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, GET, /.well-known/openid-configuration, *, *

View File

@@ -1,11 +1,11 @@
#!/bin/bash #!/bin/bash
#try to connect to google to determine whether user need to use proxy #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 ] if [ $? == 0 ]
then 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 . CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server .
else 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 . CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server .
fi fi

29
captcha/default.go Normal file
View File

@@ -0,0 +1,29 @@
// 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 captcha
import "github.com/casdoor/casdoor/object"
type DefaultCaptchaProvider struct {
}
func NewDefaultCaptchaProvider() *DefaultCaptchaProvider {
captcha := &DefaultCaptchaProvider{}
return captcha
}
func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
return object.VerifyCaptcha(clientSecret, token), nil
}

60
captcha/hcaptcha.go Normal file
View File

@@ -0,0 +1,60 @@
// 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 captcha
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)
const HCaptchaVerifyUrl = "https://hcaptcha.com/siteverify"
type HCaptchaProvider struct {
}
func NewHCaptchaProvider() *HCaptchaProvider {
captcha := &HCaptchaProvider{}
return captcha
}
func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},
}
resp, err := http.PostForm(HCaptchaVerifyUrl, reqData)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, err
}
type captchaResponse struct {
Success bool `json:"success"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
return captchaResp.Success, nil
}

30
captcha/provider.go Normal file
View File

@@ -0,0 +1,30 @@
// 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 captcha
type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error)
}
func GetCaptchaProvider(captchaType string) CaptchaProvider {
if captchaType == "Default" {
return NewDefaultCaptchaProvider()
} else if captchaType == "reCAPTCHA" {
return NewReCaptchaProvider()
} else if captchaType == "hCaptcha" {
return NewHCaptchaProvider()
}
return nil
}

60
captcha/recaptcha.go Normal file
View File

@@ -0,0 +1,60 @@
// 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 captcha
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)
const ReCaptchaVerifyUrl = "https://recaptcha.net/recaptcha/api/siteverify"
type ReCaptchaProvider struct {
}
func NewReCaptchaProvider() *ReCaptchaProvider {
captcha := &ReCaptchaProvider{}
return captcha
}
func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) {
reqData := url.Values{
"secret": {clientSecret},
"response": {token},
}
resp, err := http.PostForm(ReCaptchaVerifyUrl, reqData)
if err != nil {
return false, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, err
}
type captchaResponse struct {
Success bool `json:"success"`
}
captchaResp := &captchaResponse{}
err = json.Unmarshal(body, captchaResp)
if err != nil {
return false, err
}
return captchaResp.Success, nil
}

View File

@@ -17,6 +17,7 @@ package conf
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
@@ -61,7 +62,12 @@ func GetBeegoConfDataSourceName() string {
runningInDocker := os.Getenv("RUNNING_IN_DOCKER") runningInDocker := os.Getenv("RUNNING_IN_DOCKER")
if runningInDocker == "true" { if runningInDocker == "true" {
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal") // https://stackoverflow.com/questions/48546124/what-is-linux-equivalent-of-host-docker-internal
if runtime.GOOS == "linux" {
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "172.17.0.1")
} else {
dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal")
}
} }
return dataSourceName return dataSourceName

View File

@@ -75,12 +75,14 @@ type Response struct {
Data2 interface{} `json:"data2"` Data2 interface{} `json:"data2"`
} }
type HumanCheck struct { type Captcha struct {
Type string `json:"type"` Type string `json:"type"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
Scene string `json:"scene"` Scene string `json:"scene"`
CaptchaId string `json:"captchaId"` CaptchaId string `json:"captchaId"`
CaptchaImage interface{} `json:"captchaImage"` CaptchaImage []byte `json:"captchaImage"`
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
} }
// Signup // Signup
@@ -116,7 +118,7 @@ func (c *ApiController) Signup() {
return 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) checkResult := object.CheckVerificationCode(form.Email, form.EmailCode)
if len(checkResult) != 0 { if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("Email: %s", checkResult)) c.ResponseError(fmt.Sprintf("Email: %s", checkResult))
@@ -210,7 +212,7 @@ func (c *ApiController) Signup() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
go object.AddRecord(record) util.SafeGoroutine(func() { object.AddRecord(record) })
userId := fmt.Sprintf("%s/%s", user.Owner, user.Name) userId := fmt.Sprintf("%s/%s", user.Owner, user.Name)
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId) util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
@@ -285,25 +287,36 @@ func (c *ApiController) GetUserinfo() {
resp, err := object.GetUserInfo(userId, scope, aud, host) resp, err := object.GetUserInfo(userId, scope, aud, host)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return
} }
c.Data["json"] = resp c.Data["json"] = resp
c.ServeJSON() c.ServeJSON()
} }
// GetHumanCheck ... // GetCaptcha ...
// @Tag Login API // @Tag Login API
// @Title GetHumancheck // @Title GetCaptcha
// @router /api/get-human-check [get] // @router /api/get-captcha [get]
func (c *ApiController) GetHumanCheck() { func (c *ApiController) GetCaptcha() {
c.Data["json"] = HumanCheck{Type: "none"} applicationId := c.Input().Get("applicationId")
isCurrentProvider := c.Input().Get("isCurrentProvider")
provider := object.GetDefaultHumanCheckProvider() captchaProvider, err := object.GetCaptchaProviderByApplication(applicationId, isCurrentProvider)
if provider == nil { if err != nil {
id, img := object.GetCaptcha() c.ResponseError(err.Error())
c.Data["json"] = HumanCheck{Type: "captcha", CaptchaId: id, CaptchaImage: img}
c.ServeJSON()
return return
} }
c.ServeJSON() if captchaProvider != nil {
if captchaProvider.Type == "Default" {
id, img := object.GetCaptcha()
c.ResponseOk(Captcha{Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img})
return
} else if captchaProvider.Type != "" {
c.ResponseOk(Captcha{Type: captchaProvider.Type, ClientId: captchaProvider.ClientId, ClientSecret: captchaProvider.ClientSecret})
return
}
}
c.ResponseOk(Captcha{Type: "none"})
} }

View File

@@ -28,6 +28,7 @@ import (
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/google/uuid"
) )
func codeToResponse(code *object.Code) *Response { func codeToResponse(code *object.Code) *Response {
@@ -132,8 +133,8 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// @Param redirectUri query string true "redirect uri" // @Param redirectUri query string true "redirect uri"
// @Param scope query string true "scope" // @Param scope query string true "scope"
// @Param state query string true "state" // @Param state query string true "state"
// @Success 200 {object} controllers.api_controller.Response The Response object // @Success 200 {object} Response The Response object
// @router /update-application [get] // @router /get-app-login [get]
func (c *ApiController) GetApplicationLogin() { func (c *ApiController) GetApplicationLogin() {
clientId := c.Input().Get("clientId") clientId := c.Input().Get("clientId")
responseType := c.Input().Get("responseType") responseType := c.Input().Get("responseType")
@@ -162,9 +163,16 @@ func setHttpClient(idProvider idp.IdProvider, providerType string) {
// @Title Login // @Title Login
// @Tag Login API // @Tag Login API
// @Description login // @Description login
// @Param oAuthParams query string true "oAuth parameters" // @Param clientId query string true clientId
// @Param body body RequestForm true "Login information" // @Param responseType query string true responseType
// @Success 200 {object} controllers.api_controller.Response The Response object // @Param redirectUri query string true redirectUri
// @Param scope query string false scope
// @Param state query string false state
// @Param nonce query string false nonce
// @Param code_challenge_method query string false code_challenge_method
// @Param code_challenge query string false code_challenge
// @Param form body controllers.RequestForm true "Login information"
// @Success 200 {object} Response The Response object
// @router /login [post] // @router /login [post]
func (c *ApiController) Login() { func (c *ApiController) Login() {
resp := &Response{} resp := &Response{}
@@ -222,7 +230,11 @@ func (c *ApiController) Login() {
} }
// disable the verification code // 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) user = object.GetUserByFields(form.Organization, form.Username)
if user == nil { if user == nil {
@@ -248,7 +260,7 @@ func (c *ApiController) Login() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
go object.AddRecord(record) util.SafeGoroutine(func() { object.AddRecord(record) })
} }
} else if form.Provider != "" { } else if form.Provider != "" {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
@@ -283,7 +295,7 @@ func (c *ApiController) Login() {
clientSecret = provider.ClientSecret2 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 { if idProvider == nil {
c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type)) c.ResponseError(fmt.Sprintf("The provider type: %s is not supported", provider.Type))
return return
@@ -321,12 +333,6 @@ func (c *ApiController) Login() {
user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id)) user = object.GetUser(fmt.Sprintf("%s/%s", application.Organization, userInfo.Id))
} else if provider.Category == "OAuth" { } else if provider.Category == "OAuth" {
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id) user = object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if user == nil {
user = object.GetUserByField(application.Organization, provider.Type, userInfo.Username)
}
if user == nil {
user = object.GetUserByField(application.Organization, "name", userInfo.Username)
}
} }
if user != nil && user.IsDeleted == false { if user != nil && user.IsDeleted == false {
@@ -341,7 +347,7 @@ func (c *ApiController) Login() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
go object.AddRecord(record) util.SafeGoroutine(func() { object.AddRecord(record) })
} else if provider.Category == "OAuth" { } else if provider.Category == "OAuth" {
// Sign up via OAuth // Sign up via OAuth
if !application.EnableSignUp { if !application.EnableSignUp {
@@ -354,6 +360,19 @@ func (c *ApiController) Login() {
return 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 := map[string]string{}
properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2) properties["no"] = strconv.Itoa(len(object.GetUsers(application.Organization)) + 2)
user = &object.User{ user = &object.User{
@@ -390,7 +409,13 @@ func (c *ApiController) Login() {
record := object.NewRecord(c.Ctx) record := object.NewRecord(c.Ctx)
record.Organization = application.Organization record.Organization = application.Organization
record.User = user.Name record.User = user.Name
go object.AddRecord(record) util.SafeGoroutine(func() { object.AddRecord(record) })
record2 := object.NewRecord(c.Ctx)
record2.Action = "signup"
record2.Organization = application.Organization
record2.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record2) })
} else if provider.Category == "SAML" { } else if provider.Category == "SAML" {
resp = &Response{Status: "error", Msg: "The account does not exist"} resp = &Response{Status: "error", Msg: "The account does not exist"}
} }
@@ -403,9 +428,6 @@ func (c *ApiController) Login() {
} }
oldUser := object.GetUserByField(application.Organization, provider.Type, userInfo.Id) oldUser := object.GetUserByField(application.Organization, provider.Type, userInfo.Id)
if oldUser == nil {
oldUser = object.GetUserByField(application.Organization, provider.Type, userInfo.Username)
}
if oldUser != nil { if oldUser != nil {
c.ResponseError(fmt.Sprintf("The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)", provider.Type, userInfo.Username, userInfo.DisplayName, oldUser.Name, oldUser.DisplayName)) c.ResponseError(fmt.Sprintf("The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)", provider.Type, userInfo.Username, userInfo.DisplayName, oldUser.Name, oldUser.DisplayName))
return return
@@ -434,6 +456,11 @@ func (c *ApiController) Login() {
user := c.getCurrentUser() user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &form) resp = c.HandleLoggedIn(application, user, &form)
record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
util.SafeGoroutine(func() { object.AddRecord(record) })
} else { } else {
c.ResponseError(fmt.Sprintf("unknown authentication type (not password or provider), form = %s", util.StructToJson(form))) c.ResponseError(fmt.Sprintf("unknown authentication type (not password or provider), form = %s", util.StructToJson(form)))
return return

View File

@@ -215,7 +215,7 @@ func (c *ApiController) SyncLdapUsers() {
object.UpdateLdapSyncTime(ldapId) object.UpdateLdapSyncTime(ldapId)
exist, failed := object.SyncLdapUsers(owner, users) exist, failed := object.SyncLdapUsers(owner, users, ldapId)
c.Data["json"] = &Response{Status: "ok", Data: &LdapSyncResp{ c.Data["json"] = &Response{Status: "ok", Data: &LdapSyncResp{
Exist: *exist, Exist: *exist,
Failed: *failed, Failed: *failed,

120
controllers/model.go Normal file
View 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()
}

View File

@@ -18,6 +18,8 @@ import "github.com/casdoor/casdoor/object"
// @Title GetOidcDiscovery // @Title GetOidcDiscovery
// @Tag OIDC API // @Tag OIDC API
// @Description Get Oidc Discovery
// @Success 200 {object} object.OidcDiscovery
// @router /.well-known/openid-configuration [get] // @router /.well-known/openid-configuration [get]
func (c *RootController) GetOidcDiscovery() { func (c *RootController) GetOidcDiscovery() {
host := c.Ctx.Request.Host host := c.Ctx.Request.Host
@@ -27,6 +29,7 @@ func (c *RootController) GetOidcDiscovery() {
// @Title GetJwks // @Title GetJwks
// @Tag OIDC API // @Tag OIDC API
// @Success 200 {object} jose.JSONWebKey
// @router /.well-known/jwks [get] // @router /.well-known/jwks [get]
func (c *RootController) GetJwks() { func (c *RootController) GetJwks() {
jwks, err := object.GetJsonWebKeySet() jwks, err := object.GetJsonWebKeySet()

View File

@@ -158,3 +158,20 @@ func (c *ApiController) NotifyPayment() {
panic(fmt.Errorf("NotifyPayment() failed: %v", ok)) 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)
}

View File

@@ -26,7 +26,7 @@ import (
// @Description get all records // @Description get all records
// @Param pageSize query string true "The size of each page" // @Param pageSize query string true "The size of each page"
// @Param p query string true "The number of the page" // @Param p query string true "The number of the page"
// @Success 200 {array} object.Records The Response object // @Success 200 {object} object.Record The Response object
// @router /get-records [get] // @router /get-records [get]
func (c *ApiController) GetRecords() { func (c *ApiController) GetRecords() {
limit := c.Input().Get("pageSize") limit := c.Input().Get("pageSize")
@@ -50,8 +50,8 @@ func (c *ApiController) GetRecords() {
// @Tag Record API // @Tag Record API
// @Title GetRecordsByFilter // @Title GetRecordsByFilter
// @Description get records by filter // @Description get records by filter
// @Param body body object.Records true "filter Record message" // @Param filter body string true "filter Record message"
// @Success 200 {array} object.Records The Response object // @Success 200 {object} object.Record The Response object
// @router /get-records-filter [post] // @router /get-records-filter [post]
func (c *ApiController) GetRecordsByFilter() { func (c *ApiController) GetRecordsByFilter() {
body := string(c.Ctx.Input.RequestBody) body := string(c.Ctx.Input.RequestBody)

View File

@@ -26,6 +26,7 @@ func (c *ApiController) GetSamlMeta() {
application := object.GetApplication(paramApp) application := object.GetApplication(paramApp)
if application == nil { if application == nil {
c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp)) c.ResponseError(fmt.Sprintf("err: application %s not found", paramApp))
return
} }
metadata, _ := object.GetSamlMeta(application, host) metadata, _ := object.GetSamlMeta(application, host)
c.Data["xml"] = metadata c.Data["xml"] = metadata

View File

@@ -25,13 +25,26 @@ import (
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
type EmailForm struct {
Title string `json:"title"`
Content string `json:"content"`
Sender string `json:"sender"`
Receivers []string `json:"receivers"`
}
type SmsForm struct {
Content string `json:"content"`
Receivers []string `json:"receivers"`
OrgId string `json:"organizationId"` // e.g. "admin/built-in"
}
// SendEmail // SendEmail
// @Title SendEmail // @Title SendEmail
// @Tag Service API // @Tag Service API
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs. // @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param clientId query string true "The clientId of the application" // @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application" // @Param clientSecret query string true "The clientSecret of the application"
// @Param body body emailForm true "Details of the email request" // @Param from body controllers.EmailForm true "Details of the email request"
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /api/send-email [post] // @router /api/send-email [post]
func (c *ApiController) SendEmail() { func (c *ApiController) SendEmail() {
@@ -40,12 +53,8 @@ func (c *ApiController) SendEmail() {
return return
} }
var emailForm struct { var emailForm EmailForm
Title string `json:"title"`
Content string `json:"content"`
Sender string `json:"sender"`
Receivers []string `json:"receivers"`
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm) err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
@@ -86,7 +95,7 @@ func (c *ApiController) SendEmail() {
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs. // @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param clientId query string true "The clientId of the application" // @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application" // @Param clientSecret query string true "The clientSecret of the application"
// @Param body body smsForm true "Details of the sms request" // @Param from body controllers.SmsForm true "Details of the sms request"
// @Success 200 {object} Response object // @Success 200 {object} Response object
// @router /api/send-sms [post] // @router /api/send-sms [post]
func (c *ApiController) SendSms() { func (c *ApiController) SendSms() {
@@ -95,11 +104,7 @@ func (c *ApiController) SendSms() {
return return
} }
var smsForm struct { var smsForm SmsForm
Content string `json:"content"`
Receivers []string `json:"receivers"`
OrgId string `json:"organizationId"` // e.g. "admin/built-in"
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm) err := json.Unmarshal(c.Ctx.Input.RequestBody, &smsForm)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())

View File

@@ -16,7 +16,6 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"github.com/astaxie/beego/utils/pagination" "github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@@ -114,3 +113,18 @@ func (c *ApiController) DeleteSyncer() {
c.Data["json"] = wrapActionResponse(object.DeleteSyncer(&syncer)) c.Data["json"] = wrapActionResponse(object.DeleteSyncer(&syncer))
c.ServeJSON() 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()
}

View File

@@ -230,7 +230,7 @@ func (c *ApiController) RefreshToken() {
clientSecret = tokenRequest.ClientSecret clientSecret = tokenRequest.ClientSecret
grantType = tokenRequest.GrantType grantType = tokenRequest.GrantType
scope = tokenRequest.Scope scope = tokenRequest.Scope
refreshToken = tokenRequest.RefreshToken
} }
} }
@@ -275,21 +275,20 @@ func (c *ApiController) IntrospectToken() {
tokenValue := c.Input().Get("token") tokenValue := c.Input().Get("token")
clientId, clientSecret, ok := c.Ctx.Request.BasicAuth() clientId, clientSecret, ok := c.Ctx.Request.BasicAuth()
if !ok { if !ok {
util.LogWarning(c.Ctx, "Basic Authorization parses failed") clientId = c.Input().Get("client_id")
c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"} clientSecret = c.Input().Get("client_secret")
c.ServeJSON() if clientId == "" || clientSecret == "" {
return c.ResponseError("empty clientId or clientSecret")
return
}
} }
application := object.GetApplicationByClientId(clientId) application := object.GetApplicationByClientId(clientId)
if application == nil || application.ClientSecret != clientSecret { if application == nil || application.ClientSecret != clientSecret {
util.LogWarning(c.Ctx, "Basic Authorization failed") c.ResponseError("invalid application or wrong clientSecret")
c.Data["json"] = Response{Status: "error", Msg: "Unauthorized operation"}
c.ServeJSON()
return return
} }
token := object.GetTokenByTokenAndApplication(tokenValue, application.Name) token := object.GetTokenByTokenAndApplication(tokenValue, application.Name)
if token == nil { if token == nil {
util.LogWarning(c.Ctx, "application: %s can not find token", application.Name)
c.Data["json"] = &object.IntrospectionResponse{Active: false} c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON() c.ServeJSON()
return return
@@ -299,7 +298,6 @@ func (c *ApiController) IntrospectToken() {
// and token revoked case. but we not implement // and token revoked case. but we not implement
// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs. // TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
// refs: https://tools.ietf.org/html/rfc7009 // refs: https://tools.ietf.org/html/rfc7009
util.LogWarning(c.Ctx, "token invalid")
c.Data["json"] = &object.IntrospectionResponse{Active: false} c.Data["json"] = &object.IntrospectionResponse{Active: false}
c.ServeJSON() c.ServeJSON()
return return

View File

@@ -25,4 +25,5 @@ type TokenRequest struct {
Password string `json:"password"` Password string `json:"password"`
Tag string `json:"tag"` Tag string `json:"tag"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
RefreshToken string `json:"refresh_token"`
} }

View File

@@ -87,6 +87,17 @@ func (c *ApiController) GetUser() {
id := c.Input().Get("id") id := c.Input().Get("id")
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
email := c.Input().Get("email") 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 var user *object.User
if email == "" { if email == "" {
@@ -233,39 +244,15 @@ func (c *ApiController) SetPassword() {
newPassword := c.Ctx.Request.Form.Get("newPassword") newPassword := c.Ctx.Request.Form.Get("newPassword")
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
if requestUserId == "" {
c.ResponseError("Please login first")
return
}
userId := fmt.Sprintf("%s/%s", userOwner, userName) userId := fmt.Sprintf("%s/%s", userOwner, userName)
targetUser := object.GetUser(userId)
if targetUser == nil { hasPermission, err := object.CheckUserPermission(requestUserId, userId, true)
c.ResponseError(fmt.Sprintf("The user: %s doesn't exist", userId)) if !hasPermission {
c.ResponseError(err.Error())
return return
} }
hasPermission := false targetUser := object.GetUser(userId)
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
}
if oldPassword != "" { if oldPassword != "" {
msg := object.CheckPassword(targetUser, oldPassword) msg := object.CheckPassword(targetUser, oldPassword)

View File

@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@@ -48,20 +49,28 @@ func (c *ApiController) SendVerificationCode() {
checkUser := c.Ctx.Request.Form.Get("checkUser") checkUser := c.Ctx.Request.Form.Get("checkUser")
remoteAddr := util.GetIPFromRequest(c.Ctx.Request) remoteAddr := util.GetIPFromRequest(c.Ctx.Request)
if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || !strings.Contains(orgId, "/") || len(checkType) == 0 || len(checkId) == 0 || len(checkKey) == 0 { if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || !strings.Contains(orgId, "/") || len(checkType) == 0 {
c.ResponseError("Missing parameter.") c.ResponseError("Missing parameter.")
return return
} }
isHuman := false captchaProvider := captcha.GetCaptchaProvider(checkType)
captchaProvider := object.GetDefaultHumanCheckProvider()
if captchaProvider == nil {
isHuman = object.VerifyCaptcha(checkId, checkKey)
}
if !isHuman { if captchaProvider != nil {
c.ResponseError("Turing test failed.") if checkKey == "" {
return c.ResponseError("Missing parameter: checkKey.")
return
}
isHuman, err := captchaProvider.VerifyCaptcha(checkKey, checkId)
if err != nil {
c.ResponseError("Failed to verify captcha: %v", err)
return
}
if !isHuman {
c.ResponseError("Turing test failed.")
return
}
} }
user := c.getCurrentUser() user := c.getCurrentUser()
@@ -173,3 +182,36 @@ func (c *ApiController) ResetEmailOrPhone() {
c.Data["json"] = Response{Status: "ok"} c.Data["json"] = Response{Status: "ok"}
c.ServeJSON() c.ServeJSON()
} }
// VerifyCaptcha ...
// @Title VerifyCaptcha
// @Tag Verification API
// @router /verify-captcha [post]
func (c *ApiController) VerifyCaptcha() {
captchaType := c.Ctx.Request.Form.Get("captchaType")
captchaToken := c.Ctx.Request.Form.Get("captchaToken")
clientSecret := c.Ctx.Request.Form.Get("clientSecret")
if captchaToken == "" {
c.ResponseError("Missing parameter: captchaToken.")
return
}
if clientSecret == "" {
c.ResponseError("Missing parameter: clientSecret.")
return
}
provider := captcha.GetCaptchaProvider(captchaType)
if provider == nil {
c.ResponseError("Invalid captcha provider.")
return
}
isValid, err := provider.VerifyCaptcha(captchaToken, clientSecret)
if err != nil {
c.ResponseError("Failed to verify captcha: %v", err)
return
}
c.ResponseOk(isValid)
}

38
cred/argon2id.go Normal file
View 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
}

View File

@@ -30,6 +30,8 @@ func GetCredManager(passwordType string) CredManager {
return NewBcryptCredManager() return NewBcryptCredManager()
} else if passwordType == "pbkdf2-salt" { } else if passwordType == "pbkdf2-salt" {
return NewPbkdf2SaltCredManager() return NewPbkdf2SaltCredManager()
} else if passwordType == "argon2id" {
return NewArgon2idCredManager()
} }
return nil return nil
} }

View File

@@ -32,8 +32,8 @@ func getMd5HexDigest(s string) string {
return res return res
} }
func NewMd5UserSaltCredManager() *Sha256SaltCredManager { func NewMd5UserSaltCredManager() *Md5UserSaltCredManager {
cm := &Sha256SaltCredManager{} cm := &Md5UserSaltCredManager{}
return cm return cm
} }

View File

@@ -5,14 +5,13 @@ services:
build: build:
context: ./ context: ./
dockerfile: Dockerfile dockerfile: Dockerfile
entrypoint: /bin/sh -c './server --createDatabase=true'
ports: ports:
- "8000:8000" - "8000:8000"
depends_on: depends_on:
- db - db
environment: environment:
RUNNING_IN_DOCKER: "true" RUNNING_IN_DOCKER: "true"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes: volumes:
- ./conf:/conf/ - ./conf:/conf/
db: db:

13
go.mod
View File

@@ -4,15 +4,15 @@ go 1.16
require ( require (
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b 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/astaxie/beego v1.12.3
github.com/aws/aws-sdk-go v1.37.30 github.com/aws/aws-sdk-go v1.44.4
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
github.com/casbin/casbin/v2 v2.30.1 github.com/casbin/casbin/v2 v2.30.1
github.com/casbin/xorm-adapter/v2 v2.5.1 github.com/casbin/xorm-adapter/v2 v2.5.1
github.com/casdoor/go-sms-sender v0.2.0 github.com/casdoor/go-sms-sender v0.2.0
github.com/casdoor/goth v1.69.0-FIX1 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/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-ldap/ldap/v3 v3.3.0 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/go-sql-driver/mysql v1.5.0
github.com/golang-jwt/jwt/v4 v4.2.0 github.com/golang-jwt/jwt/v4 v4.2.0
github.com/google/uuid v1.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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/lestrrat-go/jwx v0.9.0 github.com/lestrrat-go/jwx v0.9.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/qiangmzsx/string-adapter/v2 v2.1.0 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/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0 github.com/russellhaering/gosaml2 v0.6.0
github.com/russellhaering/goxmldsig v1.1.1 github.com/russellhaering/goxmldsig v1.1.1
@@ -35,14 +33,13 @@ require (
github.com/tealeg/xlsx v1.0.5 github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4 github.com/thanhpk/randstr v1.0.4
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954 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/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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect
xorm.io/core v0.7.2 xorm.io/core v0.7.2
xorm.io/xorm v1.0.3 xorm.io/xorm v1.0.4
) )

76
go.sum
View File

@@ -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= 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 h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= 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 h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 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/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-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/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/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/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 h1:Z0SzZttfYI/raZ5O9WF3cezZJTSW4Yz4Kow9uWdyRwg=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= 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.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
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/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 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 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ=
github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA= 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/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.44.4 h1:ePN0CVJMdiz2vYUcJH96eyxRrtKGSDMgyhP6rah2OgE=
github.com/aws/aws-sdk-go v1.37.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 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 h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= 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= 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/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 h1:24Y3tfaJxWGJbxickGe3F9y2c8X1PgsQynhxGXV1f9Q=
github.com/casdoor/goth v1.69.0-FIX1/go.mod h1:Om55nRo8CkeDkPSNBbzXW4G5uI28ZUkSk5S69dPek3s= 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/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 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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.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/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/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/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/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= 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-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 h1:3zm64xMBhJBa8rXbm//q5UiGgOa4WO5XYEnU394N2Zw=
github.com/go-pay/gopay v1.5.72/go.mod h1:0qOGIJuFW7PKDOjmecwKyW0mgsVImgwB9yPJj0ilpn8= 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-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.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/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 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk= 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.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= 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 h1:erppMjjp69Rertg1zlgRbLJH1u+eCmRPxKjMZ5I8/Ro=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201208211235-fe770d50d911/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= 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.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 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= 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/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 h1:q0y8TPa/sTwtriJPRe8gWL++PuZ+XbOUuvKU+hvtTYs=
github.com/qiangmzsx/string-adapter/v2 v2.1.0/go.mod h1:PElPB7b7HnGKTsuADAffFpOQXHqjEGJz1+U1a6yR5wA= 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/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76/go.mod h1:JhtPzUhP5KGtCB2yksmxuYAD4hEWw4qGQJpucjsm3U0= 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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 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= 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-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-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-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 h1:BkypuErRT9A9I/iljuaG3/zdMjd/J6m8tKKJQtGfSdA=
golang.org/x/crypto v0.0.0-20220208233918-bba287dce954/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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= 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-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-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-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-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-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= 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-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-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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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-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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-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-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-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-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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/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-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-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-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-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-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-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-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.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.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.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.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.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.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-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-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-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-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 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/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw= xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw=
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= 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.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
View 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
}

View File

@@ -131,6 +131,7 @@ func (idp *CasdoorIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body) data, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {

109
idp/custom.go Normal file
View 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
}

View File

@@ -143,6 +143,7 @@ func (idp *DingTalkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body) data, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {

198
idp/douyin.go Normal file
View 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
}

View File

@@ -169,8 +169,11 @@ func (idp *LarkIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
req.Header.Set("Authorization", "Bearer "+token.AccessToken) req.Header.Set("Authorization", "Bearer "+token.AccessToken)
resp, err := idp.Client.Do(req) resp, err := idp.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err = ioutil.ReadAll(resp.Body) data, err = ioutil.ReadAll(resp.Body)
err = resp.Body.Close()
if err != nil { if err != nil {
return nil, err return nil, err
} }

200
idp/okta.go Normal file
View 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
}

View File

@@ -35,7 +35,7 @@ type IdProvider interface {
GetUserInfo(token *oauth2.Token) (*UserInfo, error) 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" { if typ == "GitHub" {
return NewGithubIdProvider(clientId, clientSecret, redirectUrl) return NewGithubIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Google" { } else if typ == "Google" {
@@ -72,6 +72,8 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
return NewBaiduIdProvider(clientId, clientSecret, redirectUrl) return NewBaiduIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Alipay" { } else if typ == "Alipay" {
return NewAlipayIdProvider(clientId, clientSecret, redirectUrl) return NewAlipayIdProvider(clientId, clientSecret, redirectUrl)
} else if typ == "Custom" {
return NewCustomIdProvider(clientId, clientSecret, redirectUrl, authUrl, tokenUrl, userInfoUrl)
} else if typ == "Infoflow" { } else if typ == "Infoflow" {
if subType == "Internal" { if subType == "Internal" {
return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl) return NewInfoflowInternalIdProvider(clientId, clientSecret, appId, redirectUrl)
@@ -82,8 +84,14 @@ func GetIdProvider(typ string, subType string, clientId string, clientSecret str
} }
} else if typ == "Casdoor" { } else if typ == "Casdoor" {
return NewCasdoorIdProvider(clientId, clientSecret, redirectUrl, hostUrl) 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) { } else if isGothSupport(typ) {
return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl) return NewGothIdProvider(typ, clientId, clientSecret, redirectUrl)
} else if typ == "Bilibili" {
return NewBilibiliIdProvider(clientId, clientSecret, redirectUrl)
} }
return nil return nil

View File

@@ -27,10 +27,11 @@ import (
"github.com/casdoor/casdoor/proxy" "github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/routers" "github.com/casdoor/casdoor/routers"
_ "github.com/casdoor/casdoor/routers" _ "github.com/casdoor/casdoor/routers"
"github.com/casdoor/casdoor/util"
) )
func main() { 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() flag.Parse()
object.InitAdapter(*createDatabase) object.InitAdapter(*createDatabase)
@@ -40,7 +41,7 @@ func main() {
proxy.InitHttpClient() proxy.InitHttpClient()
authz.InitAuthz() authz.InitAuthz()
go object.RunSyncUsersJob() util.SafeGoroutine(func() {object.RunSyncUsersJob()})
//beego.DelStaticPath("/static") //beego.DelStaticPath("/static")
beego.SetStaticPath("/static", "web/build/static") beego.SetStaticPath("/static", "web/build/static")

View File

@@ -138,6 +138,11 @@ func (a *Adapter) createTable() {
panic(err) panic(err)
} }
err = a.Engine.Sync2(new(Model))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider)) err = a.Engine.Sync2(new(Provider))
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -22,6 +22,14 @@ import (
"xorm.io/core" "xorm.io/core"
) )
type SignupItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
Required bool `json:"required"`
Prompted bool `json:"prompted"`
Rule string `json:"rule"`
}
type Application struct { type Application struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
@@ -257,7 +265,11 @@ func UpdateApplication(id string, application *Application) bool {
providerItem.Provider = nil 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 { if err != nil {
panic(err) panic(err)
} }

View File

@@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strings"
"github.com/casdoor/casdoor/cred" "github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@@ -195,3 +196,37 @@ func CheckUserPassword(organization string, username string, password string) (*
func filterField(field string) bool { func filterField(field string) bool {
return reFieldWhiteList.MatchString(field) 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")
}

View File

@@ -47,6 +47,31 @@ func initBuiltInOrganization() bool {
PhonePrefix: "86", PhonePrefix: "86",
DefaultAvatar: "https://casbin.org/img/casbin.svg", DefaultAvatar: "https://casbin.org/img/casbin.svg",
Tags: []string{}, Tags: []string{},
AccountItems: []*AccountItem{
{Name: "Organization", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "ID", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
{Name: "Name", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Display name", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Avatar", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "User type", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Password", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Email", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Phone", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Country/Region", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Location", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Affiliation", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Title", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Homepage", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Bio", Visible: true, ViewRule: "Public", ModifyRule: "Self"},
{Name: "Tag", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
},
} }
AddOrganization(organization) AddOrganization(organization)
return false return false
@@ -109,7 +134,7 @@ func initBuiltInApplication() {
{Name: "Display name", Visible: true, Required: true, Prompted: false, Rule: "None"}, {Name: "Display name", Visible: true, Required: true, Prompted: false, Rule: "None"},
{Name: "Password", 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: "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: "Phone", Visible: true, Required: true, Prompted: false, Rule: "None"},
{Name: "Agreement", Visible: true, Required: true, Prompted: false, Rule: "None"}, {Name: "Agreement", Visible: true, Required: true, Prompted: false, Rule: "None"},
}, },
@@ -147,7 +172,7 @@ func initBuiltInCert() {
DisplayName: "Built-in Cert", DisplayName: "Built-in Cert",
Scope: "JWT", Scope: "JWT",
Type: "x509", Type: "x509",
CryptoAlgorithm: "RSA", CryptoAlgorithm: "RS256",
BitSize: 4096, BitSize: 4096,
ExpireInYears: 20, ExpireInYears: 20,
PublicKey: tokenJwtPublicKey, PublicKey: tokenJwtPublicKey,

View File

@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr" "github.com/thanhpk/randstr"
@@ -42,6 +43,7 @@ type Ldap struct {
type ldapConn struct { type ldapConn struct {
Conn *goldap.Conn Conn *goldap.Conn
IsAD bool
} }
//type ldapGroup struct { //type ldapGroup struct {
@@ -78,6 +80,13 @@ type LdapRespUser struct {
Address string `json:"address"` Address string `json:"address"`
} }
type ldapServerType struct {
Vendorname string
Vendorversion string
IsGlobalCatalogReady string
ForestFunctionality string
}
func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser { func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
returnAnyNotEmpty := func(strs ...string) string { returnAnyNotEmpty := func(strs ...string) string {
for _, str := range strs { for _, str := range strs {
@@ -104,6 +113,45 @@ func LdapUsersToLdapRespUsers(users []ldapUser) []LdapRespUser {
return res 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) { func GetLdapConn(host string, port int, adminUser string, adminPasswd string) (*ldapConn, error) {
conn, err := goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) conn, err := goldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil { 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 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 //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)" SearchFilter := "(objectClass=posixAccount)"
SearchAttributes := []string{"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email", SearchAttributes := []string{"uidNumber", "uid", "cn", "gidNumber", "entryUUID", "mail", "email",
"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"} "emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"}
SearchFilterMsAD := "(objectClass=user)"
searchReq := goldap.NewSearchRequest(baseDn, SearchAttributesMsAD := []string{"uidNumber", "sAMAccountName", "cn", "gidNumber", "entryUUID", "mail", "email",
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false, "emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress"}
SearchFilter, SearchAttributes, nil) 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) searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -181,12 +242,16 @@ func (l *ldapConn) GetLdapUsers(baseDn string) ([]ldapUser, error) {
ldapUserItem.UidNumber = attribute.Values[0] ldapUserItem.UidNumber = attribute.Values[0]
case "uid": case "uid":
ldapUserItem.Uid = attribute.Values[0] ldapUserItem.Uid = attribute.Values[0]
case "sAMAccountName":
ldapUserItem.Uid = attribute.Values[0]
case "cn": case "cn":
ldapUserItem.Cn = attribute.Values[0] ldapUserItem.Cn = attribute.Values[0]
case "gidNumber": case "gidNumber":
ldapUserItem.GidNumber = attribute.Values[0] ldapUserItem.GidNumber = attribute.Values[0]
case "entryUUID": case "entryUUID":
ldapUserItem.Uuid = attribute.Values[0] ldapUserItem.Uuid = attribute.Values[0]
case "objectGUID":
ldapUserItem.Uuid = attribute.Values[0]
case "mail": case "mail":
ldapUserItem.Mail = attribute.Values[0] ldapUserItem.Mail = attribute.Values[0]
case "email": case "email":
@@ -300,7 +365,7 @@ func DeleteLdap(ldap *Ldap) bool {
return affected != 0 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 existUsers []LdapRespUser
var failedUsers []LdapRespUser var failedUsers []LdapRespUser
var uuids []string var uuids []string
@@ -311,6 +376,25 @@ func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]Ldap
existUuids := CheckLdapUuidExist(owner, uuids) 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 { for _, user := range users {
found := false found := false
if len(existUuids) > 0 { if len(existUuids) > 0 {
@@ -325,15 +409,14 @@ func SyncLdapUsers(owner string, users []LdapRespUser) (*[]LdapRespUser, *[]Ldap
Owner: owner, Owner: owner,
Name: buildLdapUserName(user.Uid, user.UidNumber), Name: buildLdapUserName(user.Uid, user.UidNumber),
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
Password: "123",
DisplayName: user.Cn, DisplayName: user.Cn,
Avatar: "https://casbin.org/img/casbin.svg", Avatar: organization.DefaultAvatar,
Email: user.Email, Email: user.Email,
Phone: user.Phone, Phone: user.Phone,
Address: []string{user.Address}, Address: []string{user.Address},
Affiliation: "Example Inc.", Affiliation: affiliation,
Tag: "staff", Tag: tag,
Score: 2000, Score: beego.AppConfig.DefaultInt("initScore", 2000),
Ldap: user.Uuid, Ldap: user.Uuid,
}) { }) {
failedUsers = append(failedUsers, user) failedUsers = append(failedUsers, user)

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/astaxie/beego/logs" "github.com/astaxie/beego/logs"
"github.com/casdoor/casdoor/util"
) )
type LdapAutoSynchronizer struct { type LdapAutoSynchronizer struct {
@@ -47,7 +48,7 @@ func (l *LdapAutoSynchronizer) StartAutoSync(ldapId string) error {
stopChan := make(chan struct{}) stopChan := make(chan struct{})
l.ldapIdToStopChan[ldapId] = stopChan l.ldapIdToStopChan[ldapId] = stopChan
logs.Info(fmt.Sprintf("autoSync started for %s", ldap.Id)) logs.Info(fmt.Sprintf("autoSync started for %s", ldap.Id))
go l.syncRoutine(ldap, stopChan) util.SafeGoroutine(func() {l.syncRoutine(ldap, stopChan)})
return nil 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)) logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue continue
} }
existed, failed := SyncLdapUsers(ldap.Owner, LdapUsersToLdapRespUsers(users)) existed, failed := SyncLdapUsers(ldap.Owner, LdapUsersToLdapRespUsers(users), ldap.Id)
if len(*failed) != 0 { 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) logs.Warning(fmt.Sprintf("ldap autosync,%d new users,but %d user failed during :", len(users)-len(*existed)-len(*failed), len(*failed)), *failed)
} else { } else {

122
object/model.go Normal file
View 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)
}

View File

@@ -76,7 +76,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend), UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend), JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", 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"}, ResponseModesSupported: []string{"login", "code", "link"},
GrantTypesSupported: []string{"password", "authorization_code"}, GrantTypesSupported: []string{"password", "authorization_code"},
SubjectTypesSupported: []string{"public"}, SubjectTypesSupported: []string{"public"},
@@ -105,6 +105,8 @@ func GetJsonWebKeySet() (jose.JSONWebKeySet, error) {
jwk.Key = x509Cert.PublicKey jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert} jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name jwk.KeyID = cert.Name
jwk.Algorithm = cert.CryptoAlgorithm
jwk.Use = "sig"
jwks.Keys = append(jwks.Keys, jwk) jwks.Keys = append(jwks.Keys, jwk)
} }

View File

@@ -20,6 +20,13 @@ import (
"xorm.io/core" "xorm.io/core"
) )
type AccountItem struct {
Name string `json:"name"`
Visible bool `json:"visible"`
ViewRule string `json:"viewRule"`
ModifyRule string `json:"modifyRule"`
}
type Organization struct { type Organization struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
@@ -35,6 +42,9 @@ type Organization struct {
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
MasterPassword string `xorm:"varchar(100)" json:"masterPassword"` MasterPassword string `xorm:"varchar(100)" json:"masterPassword"`
EnableSoftDeletion bool `json:"enableSoftDeletion"` EnableSoftDeletion bool `json:"enableSoftDeletion"`
IsProfilePublic bool `json:"isProfilePublic"`
AccountItems []*AccountItem `xorm:"varchar(2000)" json:"accountItems"`
} }
func GetOrganizationCount(owner, field, value string) int { func GetOrganizationCount(owner, field, value string) int {
@@ -120,14 +130,18 @@ func UpdateOrganization(id string, organization *Organization) bool {
} }
if name != organization.Name { if name != organization.Name {
applications := GetApplicationsByOrganizationName("admin", name) go func() {
for _, application := range applications { application := new(Application)
application.Organization = organization.Name application.Organization = organization.Name
UpdateApplication(application.GetId(), application) _, _ = adapter.Engine.Where("organization=?", name).Update(application)
}
user := new(User)
user.Owner = organization.Name
_, _ = adapter.Engine.Where("owner=?", name).Update(user)
}()
} }
if organization.MasterPassword != "" { if organization.MasterPassword != "" && organization.MasterPassword != "***" {
credManager := cred.GetCredManager(organization.PasswordType) credManager := cred.GetCredManager(organization.PasswordType)
if credManager != nil { if credManager != nil {
hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, "", organization.PasswordSalt) hashedPassword := credManager.GetHashedPassword(organization.MasterPassword, "", organization.PasswordSalt)
@@ -135,7 +149,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 { if err != nil {
panic(err) panic(err)
} }

View File

@@ -44,6 +44,16 @@ type Payment struct {
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(1000)" json:"message"` 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 { func GetPaymentCount(owner, field, value string) int {
@@ -197,6 +207,44 @@ func NotifyPayment(request *http.Request, body []byte, owner string, providerNam
return ok 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 { func (payment *Payment) GetId() string {
return fmt.Sprintf("%s/%s", payment.Owner, payment.Name) return fmt.Sprintf("%s/%s", payment.Owner, payment.Name)
} }

View File

@@ -30,6 +30,7 @@ type Permission struct {
Users []string `xorm:"mediumtext" json:"users"` Users []string `xorm:"mediumtext" json:"users"`
Roles []string `xorm:"mediumtext" json:"roles"` Roles []string `xorm:"mediumtext" json:"roles"`
Model string `xorm:"varchar(100)" json:"model"`
ResourceType string `xorm:"varchar(100)" json:"resourceType"` ResourceType string `xorm:"varchar(100)" json:"resourceType"`
Resources []string `xorm:"mediumtext" json:"resources"` Resources []string `xorm:"mediumtext" json:"resources"`
Actions []string `xorm:"mediumtext" json:"actions"` Actions []string `xorm:"mediumtext" json:"actions"`

View File

@@ -170,6 +170,7 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
owner := product.Owner owner := product.Owner
productName := product.Name productName := product.Name
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
paymentName := util.GenerateTimeId() paymentName := util.GenerateTimeId()
productDisplayName := product.DisplayName 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) 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) 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 { if err != nil {
return "", err return "", err
} }

View File

@@ -27,16 +27,21 @@ type Provider struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Category string `xorm:"varchar(100)" json:"category"` Category string `xorm:"varchar(100)" json:"category"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
SubType string `xorm:"varchar(100)" json:"subType"` SubType string `xorm:"varchar(100)" json:"subType"`
Method string `xorm:"varchar(100)" json:"method"` Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"` ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
ClientId2 string `xorm:"varchar(100)" json:"clientId2"` ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"` ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
Cert string `xorm:"varchar(100)" json:"cert"` 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"` Host string `xorm:"varchar(100)" json:"host"`
Port int `json:"port"` Port int `json:"port"`
@@ -137,8 +142,8 @@ func GetProvider(id string) *Provider {
return getProvider(owner, name) return getProvider(owner, name)
} }
func GetDefaultHumanCheckProvider() *Provider { func GetDefaultCaptchaProvider() *Provider {
provider := Provider{Owner: "admin", Category: "HumanCheck"} provider := Provider{Owner: "admin", Category: "Captcha"}
existed, err := adapter.Engine.Get(&provider) existed, err := adapter.Engine.Get(&provider)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -167,7 +172,14 @@ func UpdateProvider(id string, provider *Provider) bool {
return false 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 { if err != nil {
panic(err) panic(err)
} }
@@ -213,3 +225,37 @@ func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
func (p *Provider) GetId() string { func (p *Provider) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name) return fmt.Sprintf("%s/%s", p.Owner, p.Name)
} }
func GetCaptchaProviderByOwnerName(applicationId string) (*Provider, error) {
owner, name := util.GetOwnerAndNameFromId(applicationId)
provider := Provider{Owner: owner, Name: name, Category: "Captcha"}
existed, err := adapter.Engine.Get(&provider)
if err != nil {
return nil, err
}
if !existed {
return nil, fmt.Errorf("the provider: %s does not exist", applicationId)
}
return &provider, nil
}
func GetCaptchaProviderByApplication(applicationId, isCurrentProvider string) (*Provider, error) {
if isCurrentProvider == "true" {
return GetCaptchaProviderByOwnerName(applicationId)
}
application := GetApplication(applicationId)
if application == nil || len(application.Providers) == 0 {
return nil, fmt.Errorf("invalid application id")
}
for _, provider := range application.Providers {
if provider.Provider == nil {
continue
}
if provider.Provider.Category == "Captcha" {
return GetCaptchaProviderByOwnerName(fmt.Sprintf("%s/%s", provider.Provider.Owner, provider.Provider.Name))
}
}
return nil, nil
}

View File

@@ -23,7 +23,7 @@ import (
type Resource struct { type Resource struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` 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"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
@@ -31,7 +31,7 @@ type Resource struct {
Application string `xorm:"varchar(100)" json:"application"` Application string `xorm:"varchar(100)" json:"application"`
Tag string `xorm:"varchar(100)" json:"tag"` Tag string `xorm:"varchar(100)" json:"tag"`
Parent string `xorm:"varchar(100)" json:"parent"` 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"` FileType string `xorm:"varchar(100)" json:"fileType"`
FileFormat string `xorm:"varchar(100)" json:"fileFormat"` FileFormat string `xorm:"varchar(100)" json:"fileFormat"`
FileSize int `json:"fileSize"` FileSize int `json:"fileSize"`

View File

@@ -51,7 +51,7 @@ func NewSamlResponse(user *User, host string, publicKey string, destination stri
samlResponse.CreateAttr("Version", "2.0") samlResponse.CreateAttr("Version", "2.0")
samlResponse.CreateAttr("IssueInstant", now) samlResponse.CreateAttr("IssueInstant", now)
samlResponse.CreateAttr("Destination", destination) samlResponse.CreateAttr("Destination", destination)
samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("Casdoor_%s", arId)) samlResponse.CreateAttr("InResponseTo", fmt.Sprintf("_%s", arId))
samlResponse.CreateElement("saml:Issuer").SetText(host) samlResponse.CreateElement("saml:Issuer").SetText(host)
samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success") samlResponse.CreateElement("samlp:Status").CreateElement("samlp:StatusCode").CreateAttr("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
@@ -261,13 +261,15 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
} }
ctx := dsig.NewDefaultSigningContext(randomKeyStore) ctx := dsig.NewDefaultSigningContext(randomKeyStore)
ctx.Hash = crypto.SHA1 ctx.Hash = crypto.SHA1
signedXML, err := ctx.SignEnveloped(samlResponse) //signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
if err != nil { //if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) // return "", "", fmt.Errorf("err: %s", err.Error())
} //}
sig, err := ctx.ConstructSignature(samlResponse, true)
samlResponse.InsertChildAt(1, sig)
doc := etree.NewDocument() doc := etree.NewDocument()
doc.SetRoot(signedXML) doc.SetRoot(samlResponse)
xmlStr, err := doc.WriteToString() xmlStr, err := doc.WriteToString()
if err != nil { if err != nil {
return "", "", fmt.Errorf("err: %s", err.Error()) return "", "", fmt.Errorf("err: %s", err.Error())

View File

@@ -56,6 +56,9 @@ func getUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
// provider.Domain = "http://localhost:8000" or "https://door.casdoor.com" // provider.Domain = "http://localhost:8000" or "https://door.casdoor.com"
host = util.UrlJoin(provider.Domain, "/files") host = util.UrlJoin(provider.Domain, "/files")
} }
if provider.Type == "Azure Blob" {
host = fmt.Sprintf("%s/%s", host, provider.Bucket)
}
fileUrl := util.UrlJoin(host, objectKey) fileUrl := util.UrlJoin(host, objectKey)
if hasTimestamp { if hasTimestamp {

View File

@@ -133,7 +133,11 @@ func UpdateSyncer(id string, syncer *Syncer) bool {
return false 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 { if err != nil {
panic(err) panic(err)
} }
@@ -206,3 +210,8 @@ func (syncer *Syncer) getTable() string {
return syncer.Table return syncer.Table
} }
} }
func RunSyncer(syncer *Syncer) {
syncer.initAdapter()
syncer.syncUsers()
}

View File

@@ -173,7 +173,18 @@ func (syncer *Syncer) getOriginalUsersFromMap(results []map[string]string) []*Or
} }
for _, tableColumn := range syncer.TableColumns { 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" { if syncer.Type == "Keycloak" {

View File

@@ -27,6 +27,10 @@ import (
"xorm.io/core" "xorm.io/core"
) )
const (
hourSeconds = 3600
)
type Code struct { type Code struct {
Message string `xorm:"varchar(100)" json:"message"` Message string `xorm:"varchar(100)" json:"message"`
Code string `xorm:"varchar(100)" json:"code"` Code string `xorm:"varchar(100)" json:"code"`
@@ -292,7 +296,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeChallenge: challenge, CodeChallenge: challenge,
@@ -463,7 +467,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: newAccessToken, AccessToken: newAccessToken,
RefreshToken: newRefreshToken, RefreshToken: newRefreshToken,
ExpiresIn: application.ExpireInHours * 60, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
} }
@@ -572,7 +576,7 @@ func GetPasswordToken(application *Application, username string, password string
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeIsUsed: true, CodeIsUsed: true,
@@ -604,7 +608,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
User: nullUser.Name, User: nullUser.Name,
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
ExpiresIn: application.ExpireInHours * 60, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeIsUsed: true, CodeIsUsed: true,
@@ -629,7 +633,7 @@ func GetTokenByUser(application *Application, user *User, scope string, host str
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken, RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60, ExpiresIn: application.ExpireInHours * hourSeconds,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
CodeIsUsed: true, CodeIsUsed: true,

View File

@@ -94,6 +94,10 @@ type User struct {
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"` AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
Slack string `xorm:"slack varchar(100)" json:"slack"` Slack string `xorm:"slack varchar(100)" json:"slack"`
Steam string `xorm:"steam varchar(100)" json:"steam"` 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"` Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
Properties map[string]string `json:"properties"` Properties map[string]string `json:"properties"`
@@ -312,6 +316,9 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
return false return false
} }
if user.Password == "***" {
user.Password = oldUser.Password
}
user.UpdateUserHash() user.UpdateUserHash()
if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" { if user.Avatar != oldUser.Avatar && user.Avatar != "" && user.PermanentAvatar != "*" {

View File

@@ -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")
}
}

View File

@@ -29,7 +29,7 @@ func GetUserByField(organizationName string, field string, value string) *User {
} }
user := User{Owner: organizationName} 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 { if err != nil {
panic(err) panic(err)
} }

View File

@@ -45,7 +45,7 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
return pp 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 //pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{} bm := gopay.BodyMap{}
@@ -90,3 +90,7 @@ func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, auth
return productDisplayName, paymentName, price, productName, providerName, nil 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
View File

@@ -38,11 +38,14 @@ type GcPayReqInfo struct {
OrderDate string `json:"orderdate"` OrderDate string `json:"orderdate"`
OrderNo string `json:"orderno"` OrderNo string `json:"orderno"`
Amount string `json:"amount"` Amount string `json:"amount"`
PayerId string `json:"payerid"`
PayerName string `json:"payername"`
Xmpch string `json:"xmpch"` Xmpch string `json:"xmpch"`
Body string `json:"body"`
ReturnUrl string `json:"return_url"` ReturnUrl string `json:"return_url"`
NotifyUrl string `json:"notify_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 { type GcPayRespInfo struct {
@@ -87,6 +90,27 @@ type GcResponseBody struct {
Sign string `json:"sign"` 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 { func NewGcPaymentProvider(clientId string, clientSecret string, host string) *GcPaymentProvider {
pp := &GcPaymentProvider{} pp := &GcPaymentProvider{}
@@ -130,16 +154,17 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil 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{ payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(), OrderDate: util.GenerateSimpleTimeId(),
OrderNo: util.GenerateTimeId(), OrderNo: paymentName,
Amount: getPriceString(price), Amount: getPriceString(price),
PayerId: "",
PayerName: "",
Xmpch: pp.Xmpch, Xmpch: pp.Xmpch,
Body: productDisplayName,
ReturnUrl: returnUrl, ReturnUrl: returnUrl,
NotifyUrl: notifyUrl, NotifyUrl: notifyUrl,
Remark1: payerName,
Remark2: productName,
} }
b, err := json.Marshal(payReqInfo) 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 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
}

View File

@@ -17,8 +17,9 @@ package pp
import "net/http" import "net/http"
type PaymentProvider interface { 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) 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 { func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {

View File

@@ -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")) { 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" return "/cas"
} }
if strings.HasPrefix(urlPath, "/api/login/oauth") {
return "/api/login/oauth"
}
return urlPath return urlPath
} }

View File

@@ -65,5 +65,5 @@ func RecordMessage(ctx *context.Context) {
record.Organization, record.User = util.GetOwnerAndNameFromId(userId) record.Organization, record.User = util.GetOwnerAndNameFromId(userId)
} }
go object.AddRecord(record) util.SafeGoroutine(func() { object.AddRecord(record) })
} }

View File

@@ -84,12 +84,19 @@ func initAPI() {
beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission") beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission")
beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission") 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/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck") beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")
beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser") beego.Router("/api/get-ldap-user", &controllers.ApiController{}, "POST:GetLdapUser")
beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps") beego.Router("/api/get-ldaps", &controllers.ApiController{}, "POST:GetLdaps")
@@ -145,6 +152,7 @@ func initAPI() {
beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer") beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer")
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer") beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer") 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-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert") beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
@@ -166,6 +174,7 @@ func initAPI() {
beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment") beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment")
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment") 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/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-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms") beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

@@ -15,8 +15,8 @@
package storage package storage
import ( import (
"github.com/qor/oss" "github.com/casdoor/oss"
"github.com/qor/oss/aliyun" "github.com/casdoor/oss/aliyun"
) )
func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface { func NewAliyunOssStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {

View File

@@ -16,8 +16,8 @@ package storage
import ( import (
awss3 "github.com/aws/aws-sdk-go/service/s3" awss3 "github.com/aws/aws-sdk-go/service/s3"
"github.com/qor/oss" "github.com/casdoor/oss"
"github.com/qor/oss/s3" "github.com/casdoor/oss/s3"
) )
func NewAwsS3StorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface { func NewAwsS3StorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {

View File

@@ -12,12 +12,20 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package object package storage
type SignupItem struct { import (
Name string `json:"name"` "github.com/casdoor/oss"
Visible bool `json:"visible"` "github.com/casdoor/oss/azureblob"
Required bool `json:"required"` )
Prompted bool `json:"prompted"`
Rule string `json:"rule"` 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
} }

View File

@@ -20,7 +20,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/qor/oss" "github.com/casdoor/oss"
) )
var baseFolder = "files" var baseFolder = "files"

View File

@@ -14,7 +14,7 @@
package storage 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 { func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
switch providerType { switch providerType {
@@ -26,6 +26,8 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint) return NewAliyunOssStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Tencent Cloud COS": case "Tencent Cloud COS":
return NewTencentCloudCosStorageProvider(clientId, clientSecret, region, bucket, endpoint) return NewTencentCloudCosStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Azure Blob":
return NewAzureBlobStorageProvider(clientId, clientSecret, region, bucket, endpoint)
} }
return nil return nil

View File

@@ -15,8 +15,8 @@
package storage package storage
import ( import (
"github.com/qor/oss" "github.com/casdoor/oss"
"github.com/qor/oss/tencent" "github.com/casdoor/oss/tencent"
) )
func NewTencentCloudCosStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface { func NewTencentCloudCosStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,22 @@ paths:
tags: tags:
- OIDC API - OIDC API
operationId: RootController.GetJwks operationId: RootController.GetJwks
responses:
"200":
description: ""
schema:
$ref: '#/definitions/jose.JSONWebKey'
/.well-known/openid-configuration: /.well-known/openid-configuration:
get: get:
tags: tags:
- OIDC API - OIDC API
description: Get Oidc Discovery
operationId: RootController.GetOidcDiscovery operationId: RootController.GetOidcDiscovery
responses:
"200":
description: ""
schema:
$ref: '#/definitions/object.OidcDiscovery'
/api/add-application: /api/add-application:
post: post:
tags: tags:
@@ -58,6 +69,24 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.AddLdap operationId: ApiController.AddLdap
/api/add-model:
post:
tags:
- Model API
description: add model
operationId: ApiController.AddModel
parameters:
- in: body
name: body
description: The details of the model
required: true
schema:
$ref: '#/definitions/object.Model'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/add-organization: /api/add-organization:
post: post:
tags: tags:
@@ -243,11 +272,11 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/api/get-human-check: /api/api/get-captcha:
get: get:
tags: tags:
- Login API - Login API
operationId: ApiController.GetHumancheck operationId: ApiController.GetCaptcha
/api/api/reset-email-or-phone: /api/api/reset-email-or-phone:
post: post:
tags: tags:
@@ -271,11 +300,11 @@ paths:
required: true required: true
type: string type: string
- in: body - in: body
name: body name: from
description: Details of the email request description: Details of the email request
required: true required: true
schema: schema:
$ref: '#/definitions/emailForm' $ref: '#/definitions/controllers.EmailForm'
responses: responses:
"200": "200":
description: object description: object
@@ -299,11 +328,11 @@ paths:
required: true required: true
type: string type: string
- in: body - in: body
name: body name: from
description: Details of the sms request description: Details of the sms request
required: true required: true
schema: schema:
$ref: '#/definitions/smsForm' $ref: '#/definitions/controllers.SmsForm'
responses: responses:
"200": "200":
description: object description: object
@@ -382,6 +411,24 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.DeleteLdap operationId: ApiController.DeleteLdap
/api/delete-model:
post:
tags:
- Model API
description: delete model
operationId: ApiController.DeleteModel
parameters:
- in: body
name: body
description: The details of the model
required: true
schema:
$ref: '#/definitions/object.Model'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/delete-organization: /api/delete-organization:
post: post:
tags: tags:
@@ -578,6 +625,43 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/get-app-login:
get:
tags:
- Login API
description: get application login
operationId: ApiController.GetApplicationLogin
parameters:
- in: query
name: clientId
description: client id
required: true
type: string
- in: query
name: responseType
description: response type
required: true
type: string
- in: query
name: redirectUri
description: redirect uri
required: true
type: string
- in: query
name: scope
description: scope
required: true
type: string
- in: query
name: state
description: state
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/Response'
/api/get-application: /api/get-application:
get: get:
tags: tags:
@@ -700,6 +784,42 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.GetLdaps operationId: ApiController.GetLdaps
/api/get-model:
get:
tags:
- Model API
description: get model
operationId: ApiController.GetModel
parameters:
- in: query
name: id
description: The id of the model
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/object.Model'
/api/get-models:
get:
tags:
- Model API
description: get models
operationId: ApiController.GetModels
parameters:
- in: query
name: owner
description: The owner of models
required: true
type: string
responses:
"200":
description: The Response object
schema:
type: array
items:
$ref: '#/definitions/object.Model'
/api/get-organization: /api/get-organization:
get: get:
tags: tags:
@@ -901,9 +1021,7 @@ paths:
"200": "200":
description: The Response object description: The Response object
schema: schema:
type: array $ref: '#/definitions/object.Record'
items:
$ref: '#/definitions/object.Records'
/api/get-records-filter: /api/get-records-filter:
post: post:
tags: tags:
@@ -912,18 +1030,17 @@ paths:
operationId: ApiController.GetRecordsByFilter operationId: ApiController.GetRecordsByFilter
parameters: parameters:
- in: body - in: body
name: body name: filter
description: filter Record message description: filter Record message
required: true required: true
schema: schema:
$ref: '#/definitions/object.Records' type: string
type: string
responses: responses:
"200": "200":
description: The Response object description: The Response object
schema: schema:
type: array $ref: '#/definitions/object.Record'
items:
$ref: '#/definitions/object.Records'
/api/get-resource: /api/get-resource:
get: get:
tags: tags:
@@ -1216,6 +1333,23 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/object.Webhook' $ref: '#/definitions/object.Webhook'
/api/invoice-payment:
post:
tags:
- Payment API
description: invoice payment
operationId: ApiController.InvoicePayment
parameters:
- in: query
name: id
description: The id of the payment
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/login: /api/login:
post: post:
tags: tags:
@@ -1224,21 +1358,51 @@ paths:
operationId: ApiController.Login operationId: ApiController.Login
parameters: parameters:
- in: query - in: query
name: oAuthParams name: clientId
description: oAuth parameters description: clientId
required: true required: true
type: string type: string
- in: query
name: responseType
description: responseType
required: true
type: string
- in: query
name: redirectUri
description: redirectUri
required: true
type: string
- in: query
name: scope
description: scope
type: string
- in: query
name: state
description: state
type: string
- in: query
name: nonce
description: nonce
type: string
- in: query
name: code_challenge_method
description: code_challenge_method
type: string
- in: query
name: code_challenge
description: code_challenge
type: string
- in: body - in: body
name: body name: form
description: Login information description: Login information
required: true required: true
schema: schema:
$ref: '#/definitions/RequestForm' $ref: '#/definitions/controllers.RequestForm'
responses: responses:
"200": "200":
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.api_controller.Response' $ref: '#/definitions/Response'
/api/login/oauth/access_token: /api/login/oauth/access_token:
post: post:
tags: tags:
@@ -1424,6 +1588,24 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/controllers.Response' $ref: '#/definitions/controllers.Response'
/api/run-syncer:
get:
tags:
- Syncer API
description: run syncer
operationId: ApiController.RunSyncer
parameters:
- in: body
name: body
description: The details of the syncer
required: true
schema:
$ref: '#/definitions/object.Syncer'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/send-verification-code: /api/send-verification-code:
post: post:
tags: tags:
@@ -1493,42 +1675,6 @@ paths:
tags: tags:
- Login API - Login API
/api/update-application: /api/update-application:
get:
tags:
- Login API
description: get application login
operationId: ApiController.GetApplicationLogin
parameters:
- in: query
name: clientId
description: client id
required: true
type: string
- in: query
name: responseType
description: response type
required: true
type: string
- in: query
name: redirectUri
description: redirect uri
required: true
type: string
- in: query
name: scope
description: scope
required: true
type: string
- in: query
name: state
description: state
required: true
type: string
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.api_controller.Response'
post: post:
tags: tags:
- Application API - Application API
@@ -1579,6 +1725,29 @@ paths:
tags: tags:
- Account API - Account API
operationId: ApiController.UpdateLdap operationId: ApiController.UpdateLdap
/api/update-model:
post:
tags:
- Model API
description: update model
operationId: ApiController.UpdateModel
parameters:
- in: query
name: id
description: The id of the model
required: true
type: string
- in: body
name: body
description: The details of the model
required: true
schema:
$ref: '#/definitions/object.Model'
responses:
"200":
description: The Response object
schema:
$ref: '#/definitions/controllers.Response'
/api/update-organization: /api/update-organization:
post: post:
tags: tags:
@@ -1830,27 +1999,97 @@ paths:
description: The Response object description: The Response object
schema: schema:
$ref: '#/definitions/object.Userinfo' $ref: '#/definitions/object.Userinfo'
/api/verify-captcha:
post:
tags:
- Verification API
operationId: ApiController.VerifyCaptcha
definitions: definitions:
2127.0xc00036c600.false: 2200.0xc0003c4b70.false:
title: "false" title: "false"
type: object type: object
2161.0xc00036c630.false: 2235.0xc0003c4ba0.false:
title: "false" title: "false"
type: object type: object
RequestForm:
title: RequestForm
type: object
Response: Response:
title: Response title: Response
type: object type: object
controllers.EmailForm:
title: EmailForm
type: object
properties:
content:
type: string
receivers:
type: array
items:
type: string
sender:
type: string
title:
type: string
controllers.RequestForm:
title: RequestForm
type: object
properties:
affiliation:
type: string
application:
type: string
autoSignin:
type: boolean
code:
type: string
email:
type: string
emailCode:
type: string
firstName:
type: string
idCard:
type: string
lastName:
type: string
method:
type: string
name:
type: string
organization:
type: string
password:
type: string
phone:
type: string
phoneCode:
type: string
phonePrefix:
type: string
provider:
type: string
redirectUri:
type: string
region:
type: string
relayState:
type: string
samlRequest:
type: string
samlResponse:
type: string
state:
type: string
type:
type: string
username:
type: string
controllers.Response: controllers.Response:
title: Response title: Response
type: object type: object
properties: properties:
data: data:
$ref: '#/definitions/2127.0xc00036c600.false' $ref: '#/definitions/2200.0xc0003c4b70.false'
data2: data2:
$ref: '#/definitions/2161.0xc00036c630.false' $ref: '#/definitions/2235.0xc0003c4ba0.false'
msg: msg:
type: string type: string
name: name:
@@ -1859,25 +2098,33 @@ definitions:
type: string type: string
sub: sub:
type: string type: string
controllers.api_controller.Response: controllers.SmsForm:
title: Response title: SmsForm
type: object type: object
properties: properties:
data: content:
$ref: '#/definitions/2127.0xc00036c600.false' type: string
data2: organizationId:
$ref: '#/definitions/2161.0xc00036c630.false' type: string
msg: receivers:
type: array
items:
type: string
jose.JSONWebKey:
title: JSONWebKey
type: object
object.AccountItem:
title: AccountItem
type: object
properties:
modifyRule:
type: string type: string
name: name:
type: string type: string
status: viewRule:
type: string type: string
sub: visible:
type: string type: boolean
emailForm:
title: emailForm
type: object
object.Adapter: object.Adapter:
title: Adapter title: Adapter
type: object type: object
@@ -2037,10 +2284,80 @@ definitions:
type: string type: string
username: username:
type: string type: string
object.Model:
title: Model
type: object
properties:
createdTime:
type: string
displayName:
type: string
isEnabled:
type: boolean
modelText:
type: string
name:
type: string
owner:
type: string
object.OidcDiscovery:
title: OidcDiscovery
type: object
properties:
authorization_endpoint:
type: string
claims_supported:
type: array
items:
type: string
grant_types_supported:
type: array
items:
type: string
id_token_signing_alg_values_supported:
type: array
items:
type: string
introspection_endpoint:
type: string
issuer:
type: string
jwks_uri:
type: string
request_object_signing_alg_values_supported:
type: array
items:
type: string
request_parameter_supported:
type: boolean
response_modes_supported:
type: array
items:
type: string
response_types_supported:
type: array
items:
type: string
scopes_supported:
type: array
items:
type: string
subject_types_supported:
type: array
items:
type: string
token_endpoint:
type: string
userinfo_endpoint:
type: string
object.Organization: object.Organization:
title: Organization title: Organization
type: object type: object
properties: properties:
accountItems:
type: array
items:
$ref: '#/definitions/object.AccountItem'
createdTime: createdTime:
type: string type: string
defaultAvatar: defaultAvatar:
@@ -2051,6 +2368,8 @@ definitions:
type: boolean type: boolean
favicon: favicon:
type: string type: string
isProfilePublic:
type: boolean
masterPassword: masterPassword:
type: string type: string
name: name:
@@ -2081,6 +2400,16 @@ definitions:
type: string type: string
displayName: displayName:
type: string type: string
invoiceRemark:
type: string
invoiceTaxId:
type: string
invoiceTitle:
type: string
invoiceType:
type: string
invoiceUrl:
type: string
message: message:
type: string type: string
name: name:
@@ -2091,6 +2420,14 @@ definitions:
type: string type: string
payUrl: payUrl:
type: string type: string
personEmail:
type: string
personIdCard:
type: string
personName:
type: string
personPhone:
type: string
price: price:
type: number type: number
format: double format: double
@@ -2126,6 +2463,8 @@ definitions:
type: string type: string
isEnabled: isEnabled:
type: boolean type: boolean
model:
type: string
name: name:
type: string type: string
owner: owner:
@@ -2205,6 +2544,16 @@ definitions:
type: string type: string
createdTime: createdTime:
type: string type: string
customAuthUrl:
type: string
customLogo:
type: string
customScope:
type: string
customTokenUrl:
type: string
customUserInfoUrl:
type: string
displayName: displayName:
type: string type: string
domain: domain:
@@ -2264,9 +2613,35 @@ definitions:
type: boolean type: boolean
provider: provider:
$ref: '#/definitions/object.Provider' $ref: '#/definitions/object.Provider'
object.Records: object.Record:
title: Records title: Record
type: object type: object
properties:
action:
type: string
clientIp:
type: string
createdTime:
type: string
extendedUser:
$ref: '#/definitions/object.User'
id:
type: integer
format: int64
isTriggered:
type: boolean
method:
type: string
name:
type: string
organization:
type: string
owner:
type: string
requestUri:
type: string
user:
type: string
object.Role: object.Role:
title: Role title: Role
type: object type: object
@@ -2442,6 +2817,8 @@ definitions:
type: string type: string
baidu: baidu:
type: string type: string
bilibili:
type: string
bio: bio:
type: string type: string
birthday: birthday:
@@ -2452,10 +2829,14 @@ definitions:
type: string type: string
createdTime: createdTime:
type: string type: string
custom:
type: string
dingtalk: dingtalk:
type: string type: string
displayName: displayName:
type: string type: string
douyin:
type: string
education: education:
type: string type: string
email: email:
@@ -2521,6 +2902,8 @@ definitions:
type: string type: string
name: name:
type: string type: string
okta:
type: string
owner: owner:
type: string type: string
password: password:
@@ -2558,6 +2941,8 @@ definitions:
type: string type: string
type: type:
type: string type: string
unionId:
type: string
updatedTime: updatedTime:
type: string type: string
wechat: wechat:
@@ -2618,9 +3003,6 @@ definitions:
type: string type: string
url: url:
type: string type: string
smsForm:
title: smsForm
type: object
xorm.Engine: xorm.Engine:
title: Engine title: Engine
type: object type: object

38
util/util.go Normal file
View 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()
}()
}

View File

@@ -1,13 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?5998fcd123c220efc0936edf4f250504";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<!-- <link rel="icon" href="%PUBLIC_URL%/favicon.png" />--> <!-- <link rel="icon" href="%PUBLIC_URL%/favicon.png" />-->
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Casdoor - An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS"
/> />
<link rel="apple-touch-icon" href="https://cdn.casdoor.com/static/favicon.png" /> <link rel="apple-touch-icon" href="https://cdn.casdoor.com/static/favicon.png" />
<!-- <!--

242
web/src/AccountTable.js Normal file
View File

@@ -0,0 +1,242 @@
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DownOutlined, DeleteOutlined, UpOutlined} from '@ant-design/icons';
import {Button, Col, Row, Select, Switch, Table, Tooltip} from 'antd';
import * as Setting from "./Setting";
import i18next from "i18next";
const { Option } = Select;
class AccountTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
};
}
updateTable(table) {
this.props.onUpdateTable(table);
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
let row = {name: Setting.getNewRowNameForTable(table, "Please select an account item"), visible: true};
if (table === undefined) {
table = [];
}
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("provider:Name"),
dataIndex: 'name',
key: 'name',
render: (text, record, index) => {
const items = [
{name: "Organization", displayName: i18next.t("general:Organization")},
{name: "ID", displayName: i18next.t("general:ID")},
{name: "Name", displayName: i18next.t("general:Name")},
{name: "Display name", displayName: i18next.t("general:Display name")},
{name: "Avatar", displayName: i18next.t("general:Avatar")},
{name: "User type", displayName: i18next.t("general:User type")},
{name: "Password", displayName: i18next.t("general:Password")},
{name: "Email", displayName: i18next.t("general:Email")},
{name: "Phone", displayName: i18next.t("general:Phone")},
{name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{name: "Location", displayName: i18next.t("user:Location")},
{name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{name: "Title", displayName: i18next.t("user:Title")},
{name: "Homepage", displayName: i18next.t("user:Homepage")},
{name: "Bio", displayName: i18next.t("user:Bio")},
{name: "Tag", displayName: i18next.t("user:Tag")},
{name: "Signup application", displayName: i18next.t("general:Signup application")},
{name: "3rd-party logins", displayName: i18next.t("user:3rd-party logins")},
{name: "Properties", displayName: i18next.t("user:Properties")},
{name: "Is admin", displayName: i18next.t("user:Is admin")},
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
];
const getItemDisplayName = (text) => {
const item = items.filter(item => item.name === text);
if (item.length === 0) {
return "";
}
return item[0].displayName;
};
return (
<Select virtual={false} style={{width: '100%'}}
value={getItemDisplayName(text)}
onChange={value => {
this.updateField(table, index, 'name', value);
}} >
{
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
}
</Select>
)
}
},
{
title: i18next.t("provider:visible"),
dataIndex: 'visible',
key: 'visible',
width: '120px',
render: (text, record, index) => {
return (
<Switch checked={text} onChange={checked => {
this.updateField(table, index, 'visible', checked);
}} />
)
}
},
{
title: i18next.t("organization:viewRule"),
dataIndex: 'viewRule',
key: 'viewRule',
width: '155px',
render: (text, record, index) => {
if (!record.visible) {
return null;
}
let options = [
{id: 'Public', name: 'Public'},
{id: 'Self', name: 'Self'},
{id: 'Admin', name: 'Admin'},
];
return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
this.updateField(table, index, 'viewRule', value);
})}>
{
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
)
}
},
{
title: i18next.t("organization:modifyRule"),
dataIndex: 'modifyRule',
key: 'modifyRule',
width: '155px',
render: (text, record, index) => {
if (!record.visible) {
return null;
}
let options;
if (record.viewRule === "Admin") {
options = [
{id: 'Admin', name: 'Admin'},
{id: 'Immutable', name: 'Immutable'},
];
} else {
options = [
{id: 'Self', name: 'Self'},
{id: 'Admin', name: 'Admin'},
{id: 'Immutable', name: 'Immutable'},
];
}
return (
<Select virtual={false} style={{width: '100%'}} value={text} onChange={(value => {
this.updateField(table, index, 'modifyRule', value);
})}>
{
options.map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
)
}
},
{
title: i18next.t("general:Action"),
key: 'action',
width: '100px',
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
}
},
];
return (
<Table scroll={{x: 'max-content'}} rowKey="name" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: '20px'}} >
<Col span={24}>
{
this.renderTable(this.props.table)
}
</Col>
</Row>
</div>
)
}
}
export default AccountTable;

View File

@@ -69,6 +69,8 @@ import PromptPage from "./auth/PromptPage";
import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; import OdicDiscoveryPage from "./auth/OidcDiscoveryPage";
import SamlCallback from './auth/SamlCallback'; import SamlCallback from './auth/SamlCallback';
import CasLogout from "./auth/CasLogout"; import CasLogout from "./auth/CasLogout";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
const { Header, Footer } = Layout; const { Header, Footer } = Layout;
@@ -118,6 +120,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/roles' }); this.setState({ selectedMenuKey: '/roles' });
} else if (uri.includes('/permissions')) { } else if (uri.includes('/permissions')) {
this.setState({ selectedMenuKey: '/permissions' }); this.setState({ selectedMenuKey: '/permissions' });
} else if (uri.includes('/models')) {
this.setState({ selectedMenuKey: '/models' });
} else if (uri.includes('/providers')) { } else if (uri.includes('/providers')) {
this.setState({ selectedMenuKey: '/providers' }); this.setState({ selectedMenuKey: '/providers' });
} else if (uri.includes('/applications')) { } else if (uri.includes('/applications')) {
@@ -382,6 +386,13 @@ class App extends Component {
</Link> </Link>
</Menu.Item> </Menu.Item>
); );
res.push(
<Menu.Item key="/models">
<Link to="/models">
{i18next.t("general:Models")}
</Link>
</Menu.Item>
);
res.push( res.push(
<Menu.Item key="/providers"> <Menu.Item key="/providers">
<Link to="/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="/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" 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="/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" 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="/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} />)}/> <Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)}/>

View File

@@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from 'antd'; import {Button, Card, Col, Input, Popover, Row, Select, Switch, Upload} from 'antd';
import {LinkOutlined, UploadOutlined} from "@ant-design/icons"; import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend"; import * as CertBackend from "./backend/CertBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@@ -28,11 +28,13 @@ import UrlTable from "./UrlTable";
import ProviderTable from "./ProviderTable"; import ProviderTable from "./ProviderTable";
import SignupTable from "./SignupTable"; import SignupTable from "./SignupTable";
import PromptPage from "./auth/PromptPage"; import PromptPage from "./auth/PromptPage";
import copy from "copy-to-clipboard";
import {Controlled as CodeMirror} from 'react-codemirror2'; import {Controlled as CodeMirror} from 'react-codemirror2';
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
require('codemirror/theme/material-darker.css'); require('codemirror/theme/material-darker.css');
require("codemirror/mode/htmlmixed/htmlmixed"); require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
const { Option } = Select; const { Option } = Select;
@@ -48,6 +50,7 @@ class ApplicationEditPage extends React.Component {
providers: [], providers: [],
uploading: false, uploading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
samlMetadata: null,
}; };
} }
@@ -56,6 +59,7 @@ class ApplicationEditPage extends React.Component {
this.getOrganizations(); this.getOrganizations();
this.getCerts(); this.getCerts();
this.getProviders(); this.getProviders();
this.getSamlMetadata();
} }
getApplication() { getApplication() {
@@ -97,6 +101,15 @@ class ApplicationEditPage extends React.Component {
}); });
} }
getSamlMetadata() {
ApplicationBackend.getSamlMetadata("admin", this.state.applicationName)
.then((res) => {
this.setState({
samlMetadata: res,
})
});
}
parseApplicationField(key, value) { parseApplicationField(key, value) {
if (["expireInHours", "refreshExpireInHours"].includes(key)) { if (["expireInHours", "refreshExpireInHours"].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@@ -168,7 +181,7 @@ class ApplicationEditPage extends React.Component {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} : {Setting.getLabel(i18next.t("general:Logo"), i18next.t("general:Logo - Tooltip"))} :
</Col> </Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}> <Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
@@ -390,7 +403,7 @@ class ApplicationEditPage extends React.Component {
}}/> }}/>
<Upload maxCount={1} accept=".html" showUploadList={false} <Upload maxCount={1} accept=".html" showUploadList={false}
beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}> beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}>
<Button icon={<UploadOutlined />} loading={this.state.uploading}>Click to Upload</Button> <Button icon={<UploadOutlined />} loading={this.state.uploading}>{i18next.t("general:Click to Upload")}</Button>
</Upload> </Upload>
</Col> </Col>
</Row> </Row>
@@ -454,12 +467,25 @@ class ApplicationEditPage extends React.Component {
{id: "password", name: "Password"}, {id: "password", name: "Password"},
{id: "client_credentials", name: "Client Credentials"}, {id: "client_credentials", name: "Client Credentials"},
{id: "token", name: "Token"}, {id: "token", name: "Token"},
{id: "id_token",name:"ID Token"}, {id: "id_token", name: "ID Token"},
].map((item, index)=><Option key={index} value={item.id}>{item.name}</Option>) {id: "refresh_token", name: "Refresh Token"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:SAML metadata"), i18next.t("application:SAML metadata - Tooltip"))} :
</Col>
<Col span={22}>
<CodeMirror
value={this.state.samlMetadata}
options={{mode: 'xml', theme: 'default'}}
onBeforeChange={(editor, data, value) => {}}
/>
</Col>
</Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} : {Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :
@@ -479,7 +505,7 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
this.renderPreview() this.renderSignupSigninPreview()
} }
</Row> </Row>
{ {
@@ -503,29 +529,33 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} : {Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col> </Col>
{ {
this.renderPreview2() this.renderPromptPreview()
} }
</Row> </Row>
</Card> </Card>
) )
} }
renderPreview() { renderSignupSigninPreview() {
let signUpUrl = `/signup/${this.state.application.name}`; let signUpUrl = `/signup/${this.state.application.name}`;
let signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`; let signInUrl = `/login/oauth/authorize?client_id=${this.state.application.clientId}&response_type=code&redirect_uri=${this.state.application.redirectUris[0]}&scope=read&state=casdoor`;
let maskStyle = {position: 'absolute', top: '0px', left: '0px', zIndex: 10, height: '100%', width: '100%', background: 'rgba(0,0,0,0.4)'};
if (!this.state.application.enablePassword) { if (!this.state.application.enablePassword) {
signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize"); signUpUrl = signInUrl.replace("/login/oauth/authorize", "/signup/oauth/authorize");
} }
if (!Setting.isMobile()) {
return ( return (
<React.Fragment> <React.Fragment>
<Col span={11} style={{display:"flex", flexDirection: "column"}}> <Col span={11}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}> <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Button type="primary">{i18next.t("application:Test signup page..")}</Button> copy(`${window.location.origin}${signUpUrl}`);
</a> Setting.showMessage("success", i18next.t("application:Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
>
{i18next.t("application:Copy signup page URL")}
</Button>
<br/> <br/>
<br/> <div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
{ {
this.state.application.enablePassword ? ( this.state.application.enablePassword ? (
<SignupPage application={this.state.application} /> <SignupPage application={this.state.application} />
@@ -533,64 +563,45 @@ class ApplicationEditPage extends React.Component {
<LoginPage type={"login"} mode={"signup"} application={this.state.application} /> <LoginPage type={"login"} mode={"signup"} application={this.state.application} />
) )
} }
<div style={maskStyle}></div>
</div> </div>
</Col> </Col>
<Col span={11} style={{display:"flex", flexDirection: "column"}}> <Col span={11}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}> <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<Button type="primary">{i18next.t("application:Test signin page..")}</Button> copy(`${window.location.origin}${signInUrl}`);
</a> Setting.showMessage("success", i18next.t("application:Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
}}
>
{i18next.t("application:Copy signin page URL")}
</Button>
<br/> <br/>
<br/> <div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems:"center", overflow:"auto", flexDirection:"column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} />
</div>
</Col>
</React.Fragment>
)
} else{
return(
<React.Fragment>
<Col span={24} style={{display:"flex", flexDirection: "column"}}>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signUpUrl}>
<Button type="primary">{i18next.t("application:Test signup page..")}</Button>
</a>
<div style={{marginBottom:"10px", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
{
this.state.application.enablePassword ? (
<SignupPage application={this.state.application} />
) : (
<LoginPage type={"login"} mode={"signup"} application={this.state.application} />
)
}
</div>
<a style={{marginBottom: "10px", display: "flex"}} target="_blank" rel="noreferrer" href={signInUrl}>
<Button type="primary">{i18next.t("application:Test signin page..")}</Button>
</a>
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", alignItems: "center", overflow: "auto", flexDirection: "column", flex: "auto"}}>
<LoginPage type={"login"} mode={"signin"} application={this.state.application} /> <LoginPage type={"login"} mode={"signin"} application={this.state.application} />
<div style={maskStyle}></div>
</div> </div>
</Col> </Col>
</React.Fragment> </React.Fragment>
) )
} }
}
renderPreview2() { renderPromptPreview() {
let promptUrl = `/prompt/${this.state.application.name}`; let promptUrl = `/prompt/${this.state.application.name}`;
let maskStyle = {position: 'absolute', top: '0px', left: '0px', zIndex: 10, height: '100%', width: '100%', background: 'rgba(0,0,0,0.4)'};
return ( return (
<React.Fragment> <Col span={11}>
<Col span={(Setting.isMobile()) ? 24 : 11} style={{display:"flex", flexDirection: "column", flex: "auto"}} > <Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
<a style={{marginBottom: "10px"}} target="_blank" rel="noreferrer" href={promptUrl}> copy(`${window.location.origin}${promptUrl}`);
<Button type="primary">{i18next.t("application:Test prompt page..")}</Button> Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
</a> }}
<br style={(Setting.isMobile()) ? {display: "none"} : {}} /> >
<br style={(Setting.isMobile()) ? {display: "none"} : {}} /> {i18next.t("application:Copy prompt page URL")}
<div style={{width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}> </Button>
<PromptPage application={this.state.application} account={this.props.account} /> <br/>
</div> <div style={{position: "relative", width: "90%", border: "1px solid rgb(217,217,217)", boxShadow: "10px 10px 5px #888888", flexDirection: "column", flex: "auto"}}>
</Col> <PromptPage application={this.state.application} account={this.props.account} />
</React.Fragment> <div style={maskStyle}></div>
</div>
</Col>
) )
} }

View File

@@ -43,7 +43,7 @@ class ApplicationListPage extends BaseListPage {
{name: "Display name", visible: true, required: true, rule: "None"}, {name: "Display name", visible: true, required: true, rule: "None"},
{name: "Password", visible: true, required: true, rule: "None"}, {name: "Password", visible: true, required: true, rule: "None"},
{name: "Confirm 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: "Phone", visible: true, required: true, rule: "None"},
{name: "Agreement", visible: true, required: true, rule: "None"}, {name: "Agreement", visible: true, required: true, rule: "None"},
], ],

View File

@@ -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>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

@@ -32,7 +32,7 @@ class CertListPage extends BaseListPage {
displayName: `New Cert - ${randomName}`, displayName: `New Cert - ${randomName}`,
scope: "JWT", scope: "JWT",
type: "x509", type: "x509",
cryptoAlgorithm: "RSA", cryptoAlgorithm: "RS256",
bitSize: 4096, bitSize: 4096,
expireInYears: 20, expireInYears: 20,
publicKey: "", publicKey: "",
@@ -131,7 +131,7 @@ class CertListPage extends BaseListPage {
key: 'cryptoAlgorithm', key: 'cryptoAlgorithm',
filterMultiple: false, filterMultiple: false,
filters: [ filters: [
{text: 'RSA', value: 'RSA'}, {text: 'RS256', value: 'RS256'},
], ],
width: '190px', width: '190px',
sorter: true, sorter: true,

View File

@@ -27,7 +27,6 @@ export const CropperDiv = (props) => {
const [confirmLoading, setConfirmLoading] = React.useState(false); const [confirmLoading, setConfirmLoading] = React.useState(false);
const {title} = props; const {title} = props;
const {user} = props; const {user} = props;
const {account} = props;
const {buttonText} = props; const {buttonText} = props;
let uploadButton; let uploadButton;

208
web/src/ModelEditPage.js Normal file
View 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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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 text"), i18next.t("model:Model text - 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
View 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")}&nbsp;&nbsp;&nbsp;&nbsp;
<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;

View File

@@ -20,6 +20,7 @@ import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons"; import {LinkOutlined} from "@ant-design/icons";
import LdapTable from "./LdapTable"; import LdapTable from "./LdapTable";
import AccountTable from "./AccountTable";
const { Option } = Select; const { Option } = Select;
@@ -117,7 +118,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
@@ -127,7 +128,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
@@ -155,7 +156,7 @@ class OrganizationEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.organization.passwordType} onChange={(value => {this.updateOrganizationField('passwordType', value);})}> <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>) .map((item, index) => <Option key={index} value={item}>{item}</Option>)
} }
</Select> </Select>
@@ -187,7 +188,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :
</Col> </Col>
<Col span={23} > <Col span={23} >
@@ -197,7 +198,7 @@ class OrganizationEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{i18next.t("general:Preview")}: {i18next.t("general:Preview")}:
</Col> </Col>
<Col span={23} > <Col span={23} >
@@ -240,6 +241,28 @@ class OrganizationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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("organization:Account items"), i18next.t("organization:Account items - Tooltip"))} :
</Col>
<Col span={22} >
<AccountTable
title={i18next.t("organization:Account items")}
table={this.state.organization.accountItems}
onUpdateTable={(value) => { this.updateOrganizationField('accountItems', value)}}
/>
</Col>
</Row>
<Row style={{marginTop: '20px'}}> <Row style={{marginTop: '20px'}}>
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} : {Setting.getLabel(i18next.t("general:LDAPs"), i18next.t("general:LDAPs - Tooltip"))} :

View File

@@ -39,6 +39,32 @@ class OrganizationListPage extends BaseListPage {
tags: [], tags: [],
masterPassword: "", masterPassword: "",
enableSoftDeletion: false, enableSoftDeletion: false,
isProfilePublic: true,
accountItems: [
{name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"},
{name: "Name", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Display name", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Avatar", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "User type", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Password", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Email", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Phone", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Country/Region", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Location", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Affiliation", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Title", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Homepage", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"},
{name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is global admin", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is forbidden", visible: true, viewRule: "Admin", modifyRule: "Admin"},
{name: "Is deleted", visible: true, viewRule: "Admin", modifyRule: "Admin"},
],
} }
} }

View File

@@ -13,11 +13,14 @@
// limitations under the License. // limitations under the License.
import React from "react"; 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 PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
const { Option } = Select;
class PaymentEditPage extends React.Component { class PaymentEditPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -26,6 +29,8 @@ class PaymentEditPage extends React.Component {
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName, organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
paymentName: props.match.params.paymentName, paymentName: props.match.params.paymentName,
payment: null, payment: null,
isModalVisible: false,
isInvoiceLoading: false,
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
} }
@@ -40,6 +45,8 @@ class PaymentEditPage extends React.Component {
this.setState({ this.setState({
payment: payment, 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() { renderPayment() {
return ( return (
<Card size="small" title={ <Card size="small" title={
@@ -75,7 +158,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('organization', e.target.value);
}} /> }} />
</Col> </Col>
@@ -85,7 +168,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('name', e.target.value);
}} /> }} />
</Col> </Col>
@@ -95,7 +178,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); this.updatePaymentField('displayName', e.target.value);
}} /> }} />
</Col> </Col>
@@ -105,7 +188,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} : {Setting.getLabel(i18next.t("general:Provider"), i18next.t("general:Provider - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('provider', e.target.value);
}} /> }} />
</Col> </Col>
@@ -115,7 +198,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Type"), i18next.t("payment:Type - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Type"), i18next.t("payment:Type - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('type', e.target.value);
}} /> }} />
</Col> </Col>
@@ -125,7 +208,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Product"), i18next.t("payment:Product - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('productName', e.target.value);
}} /> }} />
</Col> </Col>
@@ -135,7 +218,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Price"), i18next.t("payment:Price - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Price"), i18next.t("payment:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('amount', e.target.value);
}} /> }} />
</Col> </Col>
@@ -145,7 +228,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('currency', e.target.value);
}} /> }} />
</Col> </Col>
@@ -155,7 +238,7 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:State"), i18next.t("payment:State - Tooltip"))} : {Setting.getLabel(i18next.t("payment:State"), i18next.t("payment:State - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('state', e.target.value);
}} /> }} />
</Col> </Col>
@@ -165,18 +248,200 @@ class PaymentEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Message"), i18next.t("payment:Message - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Message"), i18next.t("payment:Message - Tooltip"))} :
</Col> </Col>
<Col span={22} > <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); // this.updatePaymentField('message', e.target.value);
}} /> }} />
</Col> </Col>
</Row> </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> </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) { submitPaymentEdit(willExist) {
const errorText = this.checkError();
if (errorText !== "") {
Setting.showMessage("error", errorText);
return;
}
let payment = Setting.deepCopy(this.state.payment); 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) => { .then((res) => {
if (res.msg === "") { if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`); Setting.showMessage("success", `Successfully saved`);
@@ -215,6 +480,9 @@ class PaymentEditPage extends React.Component {
{ {
this.state.payment !== null ? this.renderPayment() : null this.state.payment !== null ? this.renderPayment() : null
} }
{
this.renderModal()
}
<div style={{marginTop: '20px', marginLeft: '40px'}}> <div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button size="large" onClick={() => this.submitPaymentEdit(false)}>{i18next.t("general:Save")}</Button> <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> <Button style={{marginLeft: '20px'}} type="primary" size="large" onClick={() => this.submitPaymentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>

View File

@@ -20,6 +20,7 @@ import * as UserBackend from "./backend/UserBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import * as RoleBackend from "./backend/RoleBackend"; import * as RoleBackend from "./backend/RoleBackend";
import * as ModelBackend from "./backend/ModelBackend";
const { Option } = Select; const { Option } = Select;
@@ -34,6 +35,7 @@ class PermissionEditPage extends React.Component {
organizations: [], organizations: [],
users: [], users: [],
roles: [], roles: [],
models: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
}; };
} }
@@ -52,6 +54,7 @@ class PermissionEditPage extends React.Component {
this.getUsers(permission.owner); this.getUsers(permission.owner);
this.getRoles(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) { parsePermissionField(key, value) {
if ([""].includes(key)) { if ([""].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
@@ -146,6 +158,20 @@ class PermissionEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </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'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} : {Setting.getLabel(i18next.t("role:Sub users"), i18next.t("role:Sub users - Tooltip"))} :

View File

@@ -17,7 +17,6 @@ import {Button, Descriptions, Spin} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import * as ProductBackend from "./backend/ProductBackend"; import * as ProductBackend from "./backend/ProductBackend";
import * as ProviderBackend from "./backend/ProviderBackend"; import * as ProviderBackend from "./backend/ProviderBackend";
import * as Provider from "./auth/Provider";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
class ProductBuyPage extends React.Component { class ProductBuyPage extends React.Component {
@@ -148,7 +147,7 @@ class ProductBuyPage extends React.Component {
return ( return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={ <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"} > } size={"large"} >
{ {
text text
@@ -207,7 +206,7 @@ class ProductBuyPage extends React.Component {
<Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item> <Descriptions.Item label={i18next.t("product:Tag")}><span style={{fontSize: 16}}>{product?.tag}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item> <Descriptions.Item label={i18next.t("product:SKU")}><span style={{fontSize: 16}}>{product?.name}</span></Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Image")} span={3}> <Descriptions.Item label={i18next.t("product:Image")} span={3}>
<img src={product?.image} alt={product?.image} height={90} style={{marginBottom: '20px'}}/> <img src={product?.image} alt={product?.name} height={90} style={{marginBottom: '20px'}}/>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={i18next.t("product:Price")}> <Descriptions.Item label={i18next.t("product:Price")}>
<span style={{fontSize: 28, color: "red", fontWeight: "bold"}}> <span style={{fontSize: 28, color: "red", fontWeight: "bold"}}>

View File

@@ -110,7 +110,7 @@ class ProductEditPage extends React.Component {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} : {Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
</Col> </Col>
<Col span={22} style={(Setting.isMobile()) ? {maxWidth:'100%'} :{}}> <Col span={22} style={(Setting.isMobile()) ? {maxWidth: '100%'} :{}}>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 1}>
{Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} : {Setting.getLabel(i18next.t("general:URL"), i18next.t("general:URL - Tooltip"))} :

View File

@@ -20,6 +20,7 @@ import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import { authConfig } from "./auth/Auth"; import { authConfig } from "./auth/Auth";
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { CaptchaPreview } from "./common/CaptchaPreview";
const { Option } = Select; const { Option } = Select;
const { TextArea } = Input; const { TextArea } = Input;
@@ -70,10 +71,15 @@ class ProviderEditPage extends React.Component {
case "Email": case "Email":
return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip")); return Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"));
case "SMS": case "SMS":
if (this.state.provider.type === "Volc Engine SMS") if (this.state.provider.type === "Volc Engine SMS") {
return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip")); return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip"));
if (this.state.provider.type === "Huawei Cloud SMS") } else if (this.state.provider.type === "Huawei Cloud SMS") {
return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip")); return Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
}
case "Captcha":
return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip"));
default: default:
return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip")); return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip"));
} }
@@ -84,10 +90,15 @@ class ProviderEditPage extends React.Component {
case "Email": case "Email":
return Setting.getLabel(i18next.t("login:Password"), i18next.t("login:Password - Tooltip")); return Setting.getLabel(i18next.t("login:Password"), i18next.t("login:Password - Tooltip"));
case "SMS": case "SMS":
if (this.state.provider.type === "Volc Engine SMS") if (this.state.provider.type === "Volc Engine SMS") {
return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip")); return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip"));
if (this.state.provider.type === "Huawei Cloud SMS") } else if (this.state.provider.type === "Huawei Cloud SMS") {
return Setting.getLabel(i18next.t("provider:App secret"), i18next.t("provider:AppSecret - Tooltip")); return Setting.getLabel(i18next.t("provider:App secret"), i18next.t("provider:AppSecret - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
case "Captcha":
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
default: default:
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
} }
@@ -187,6 +198,8 @@ class ProviderEditPage extends React.Component {
this.updateProviderField('domain', Setting.getFullServerUrl()); this.updateProviderField('domain', Setting.getFullServerUrl());
} else if (value === "SAML") { } else if (value === "SAML") {
this.updateProviderField('type', 'Aliyun IDaaS'); this.updateProviderField('type', 'Aliyun IDaaS');
} else if (value === "Captcha") {
this.updateProviderField('type', 'Default');
} }
})}> })}>
{ {
@@ -197,6 +210,7 @@ class ProviderEditPage extends React.Component {
{id: 'Storage', name: 'Storage'}, {id: 'Storage', name: 'Storage'},
{id: 'SAML', name: 'SAML'}, {id: 'SAML', name: 'SAML'},
{id: 'Payment', name: 'Payment'}, {id: 'Payment', name: 'Payment'},
{id: 'Captcha', name: 'Captcha'},
].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>) ].map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
} }
</Select> </Select>
@@ -212,6 +226,12 @@ class ProviderEditPage extends React.Component {
if (value === "Local File System") { if (value === "Local File System") {
this.updateProviderField('domain', Setting.getFullServerUrl()); 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>) Setting.getProviderTypeOptions(this.state.provider.category).map((providerType, index) => <Option key={index} value={providerType.id}>{providerType.name}</Option>)
@@ -256,26 +276,104 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) )
} }
<Row style={{marginTop: '20px'}} > {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> this.state.provider.type !== "Custom" ? null : (
{this.getClientIdLabel()} <React.Fragment>
</Col> <Row style={{marginTop: '20px'}} >
<Col span={22} > <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Input value={this.state.provider.clientId} onChange={e => { {Setting.getLabel(i18next.t("provider:Auth URL"), i18next.t("provider:Auth URL - Tooltip"))}
this.updateProviderField('clientId', e.target.value); </Col>
}} /> <Col span={22} >
</Col> <Input value={this.state.provider.customAuthUrl} onChange={e => {
</Row> this.updateProviderField('customAuthUrl', e.target.value);
<Row style={{marginTop: '20px'}} > }} />
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> </Col>
{this.getClientSecretLabel()} </Row>
</Col> <Row style={{marginTop: '20px'}} >
<Col span={22} > <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Input value={this.state.provider.clientSecret} onChange={e => { {Setting.getLabel(i18next.t("provider:Scope"), i18next.t("provider:Scope - Tooltip"))}
this.updateProviderField('clientSecret', e.target.value); </Col>
}} /> <Col span={22} >
</Col> <Input value={this.state.provider.customScope} onChange={e => {
</Row> 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 : 1}>
{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 : 1}>
{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>
)
}
{
this.state.provider.type !== "Default" &&
<>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientIdLabel()}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientId} onChange={e => {
this.updateProviderField('clientId', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{this.getClientSecretLabel()}
</Col>
<Col span={22} >
<Input value={this.state.provider.clientSecret} onChange={e => {
this.updateProviderField('clientSecret', e.target.value);
}} />
</Col>
</Row>
</>
}
{ {
this.state.provider.type !== "WeChat" ? null : ( this.state.provider.type !== "WeChat" ? null : (
<React.Fragment> <React.Fragment>
@@ -303,7 +401,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'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}> <Col style={{marginTop: '5px'}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} : {Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@@ -542,16 +640,39 @@ class ProviderEditPage extends React.Component {
) : null ) : null
} }
{this.getAppIdRow()} {this.getAppIdRow()}
<Row style={{marginTop: '20px'}} > {
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> this.state.provider.type !== "Default" &&
{Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} : <Row style={{marginTop: '20px'}} >
</Col> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Col span={22} > {Setting.getLabel(i18next.t("provider:Provider URL"), i18next.t("provider:Provider URL - Tooltip"))} :
<Input prefix={<LinkOutlined/>} value={this.state.provider.providerUrl} onChange={e => { </Col>
this.updateProviderField('providerUrl', e.target.value); <Col span={22} >
}} /> <Input prefix={<LinkOutlined/>} value={this.state.provider.providerUrl} onChange={e => {
</Col> this.updateProviderField('providerUrl', e.target.value);
</Row> }} />
</Col>
</Row>
}
{
this.state.provider.category === "Captcha" &&
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Preview"), i18next.t("general:Preview - Tooltip"))} :
</Col>
<Col span={22} >
<CaptchaPreview
provider={this.state.provider}
providerName={this.state.providerName}
clientSecret={this.state.provider.clientSecret}
captchaType={this.state.provider.type}
owner={this.state.provider.owner}
clientId={this.state.provider.clientId}
name={this.state.provider.name}
providerUrl={this.state.provider.providerUrl}
/>
</Col>
</Row>
}
</Card> </Card>
) )
} }

View File

@@ -134,6 +134,7 @@ class ProviderListPage extends BaseListPage {
{text: 'SMS', value: 'SMS', children: Setting.getProviderTypeOptions('SMS').map((o) => {return {text:o.id, value:o.name}})}, {text: 'SMS', value: 'SMS', children: Setting.getProviderTypeOptions('SMS').map((o) => {return {text:o.id, value:o.name}})},
{text: 'Storage', value: 'Storage', children: Setting.getProviderTypeOptions('Storage').map((o) => {return {text:o.id, value:o.name}})}, {text: 'Storage', value: 'Storage', children: Setting.getProviderTypeOptions('Storage').map((o) => {return {text:o.id, value:o.name}})},
{text: 'SAML', value: 'SAML', children: Setting.getProviderTypeOptions('SAML').map((o) => {return {text:o.id, value:o.name}})}, {text: 'SAML', value: 'SAML', children: Setting.getProviderTypeOptions('SAML').map((o) => {return {text:o.id, value:o.name}})},
{text: 'Captcha', value: 'Captcha', children: Setting.getProviderTypeOptions('Captcha').map((o) => {return {text:o.id, value:o.name}})},
], ],
sorter: true, sorter: true,
render: (text, record, index) => { render: (text, record, index) => {

View File

@@ -74,7 +74,7 @@ class ResourceListPage extends BaseListPage {
renderUpload() { renderUpload() {
return ( 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)}}> beforeUpload={file => {return false}} onChange={info => {this.handleUpload(info)}}>
<Button icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small"> <Button icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
{i18next.t("resource:Upload a file...")} {i18next.t("resource:Upload a file...")}
@@ -206,7 +206,7 @@ class ResourceListPage extends BaseListPage {
render: (text, record, index) => { render: (text, record, index) => {
if (record.fileType === "image") { if (record.fileType === "image") {
return ( return (
<a target="_blank" href={record.url}> <a target="_blank" rel="noreferrer" href={record.url}>
<img src={record.url} alt={record.name} width={100} /> <img src={record.url} alt={record.name} width={100} />
</a> </a>
) )

View File

@@ -21,7 +21,6 @@ import i18next from "i18next";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import {authConfig} from "./auth/Auth"; import {authConfig} from "./auth/Auth";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import moment from "moment";
import * as Conf from "./Conf"; import * as Conf from "./Conf";
export let ServerUrl = ""; export let ServerUrl = "";
@@ -32,6 +31,97 @@ export const StaticBaseUrl = "https://cdn.casbin.org";
// https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html // https://catamphetamine.gitlab.io/country-flag-icons/3x2/index.html
export const CountryRegionData = getCountryRegionData(); export const CountryRegionData = getCountryRegionData();
export const 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"
},
},
Captcha: {
"Default": {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://pkg.go.dev/github.com/dchest/captcha",
},
"reCAPTCHA": {
logo: `${StaticBaseUrl}/img/social_recaptcha.png`,
url: "https://www.google.com/recaptcha",
},
"hCaptcha": {
logo: `${StaticBaseUrl}/img/social_hcaptcha.png`,
url: "https://www.hcaptcha.com",
}
}
};
export function getCountryRegionData() { export function getCountryRegionData() {
let language = i18next.language; let language = i18next.language;
if (language === null || language === "null") { if (language === null || language === "null") {
@@ -115,6 +205,17 @@ export function getSignupItem(application, itemName) {
return signupItems[0]; 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) { export function isValidEmail(email) {
// https://github.com/yiminghe/async-validator/blob/057b0b047f88fac65457bae691d6cb7c6fe48ce1/src/rule/type.ts#L9 // 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,}))$/; 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 +223,36 @@ export function isValidEmail(email) {
} }
export function isValidPhone(phone) { export function isValidPhone(phone) {
if (phone === "") {
return false;
}
// https://learnku.com/articles/31543, `^s*$` filter empty email individually. // 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}$/; 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); 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) { export function isAffiliationPrompted(application) {
const signupItem = getSignupItem(application, "Affiliation"); const signupItem = getSignupItem(application, "Affiliation");
if (signupItem === null) { if (signupItem === null) {
@@ -199,11 +325,25 @@ export function openLink(link) {
w.location.href = 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) { export function goToLink(link) {
window.location.href = link; window.location.href = link;
} }
export function goToLinkSoft(ths, link) { export function goToLinkSoft(ths, link) {
if (link.startsWith("http")) {
openLink(link);
return;
}
ths.props.history.push(link); ths.props.history.push(link);
} }
@@ -214,6 +354,8 @@ export function showMessage(type, text) {
message.success(text); message.success(text);
} else if (type === "error") { } else if (type === "error") {
message.error(text); message.error(text);
} else if (type === "info") {
message.info(text);
} }
} }
@@ -345,27 +487,26 @@ export function changeLanguage(language) {
} }
export function changeMomentLanguage(language) { export function changeMomentLanguage(language) {
return; // if (language === "zh") {
if (language === "zh") { // moment.locale("zh", {
moment.locale("zh", { // relativeTime: {
relativeTime: { // future: "%s内",
future: "%s内", // past: "%s前",
past: "%s前", // s: "几秒",
s: "几秒", // ss: "%d秒",
ss: "%d秒", // m: "1分钟",
m: "1分钟", // mm: "%d分钟",
mm: "%d分钟", // h: "1小时",
h: "1小时", // hh: "%d小时",
hh: "%d小时", // d: "1天",
d: "1天", // dd: "%d天",
dd: "%d天", // M: "1个月",
M: "1个月", // MM: "%d个月",
MM: "%d个月", // y: "1年",
y: "1年", // yy: "%d年",
yy: "%d年", // },
}, // });
}); // }
}
} }
export function getClickable(text) { export function getClickable(text) {
@@ -380,9 +521,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) { export function getProviderLogo(provider) {
const idp = provider.type.toLowerCase().trim().split(' ')[0]; const idp = provider.type.toLowerCase().trim().split(' ')[0];
const url = `${StaticBaseUrl}/img/social_${idp}.png`; const url = getProviderLogoURL(provider);
return ( return (
<img width={30} height={30} src={url} alt={idp} /> <img width={30} height={30} src={url} alt={idp} />
) )
@@ -414,6 +566,10 @@ export function getProviderTypeOptions(category) {
{id: 'AzureAD', name: 'AzureAD'}, {id: 'AzureAD', name: 'AzureAD'},
{id: 'Slack', name: 'Slack'}, {id: 'Slack', name: 'Slack'},
{id: 'Steam', name: 'Steam'}, {id: 'Steam', name: 'Steam'},
{id: 'Bilibili', name: 'Bilibili'},
{id: 'Okta', name: 'Okta'},
{id: 'Douyin', name: 'Douyin'},
{id: 'Custom', name: 'Custom'},
] ]
); );
} else if (category === "Email") { } else if (category === "Email") {
@@ -438,6 +594,7 @@ export function getProviderTypeOptions(category) {
{id: 'AWS S3', name: 'AWS S3'}, {id: 'AWS S3', name: 'AWS S3'},
{id: 'Aliyun OSS', name: 'Aliyun OSS'}, {id: 'Aliyun OSS', name: 'Aliyun OSS'},
{id: 'Tencent Cloud COS', name: 'Tencent Cloud COS'}, {id: 'Tencent Cloud COS', name: 'Tencent Cloud COS'},
{id: 'Azure Blob', name: 'Azure Blob'}
] ]
); );
} else if (category === "SAML") { } else if (category === "SAML") {
@@ -452,6 +609,12 @@ export function getProviderTypeOptions(category) {
{id: 'PayPal', name: 'PayPal'}, {id: 'PayPal', name: 'PayPal'},
{id: 'GC', name: 'GC'}, {id: 'GC', name: 'GC'},
]); ]);
} else if (category === "Captcha") {
return ([
{id: 'Default', name: 'Default'},
{id: 'reCAPTCHA', name: 'reCAPTCHA'},
{id: 'hCaptcha', name: 'hCaptcha'},
]);
} else { } else {
return []; return [];
} }
@@ -660,6 +823,15 @@ export function getFromLink() {
return from; return from;
} }
export function scrollToDiv(divId) {
if (divId) {
let ele = document.getElementById(divId);
if (ele) {
ele.scrollIntoView({behavior: "smooth"});
}
}
}
export function getSyncerTableColumns(syncer) { export function getSyncerTableColumns(syncer) {
switch (syncer.type) { switch (syncer.type) {
case "Keycloak": case "Keycloak":
@@ -683,7 +855,7 @@ export function getSyncerTableColumns(syncer) {
] ]
}, },
{ {
"name":"USERNAME", "name":"LAST_NAME+FIRST_NAME",
"type":"string", "type":"string",
"casdoorName":"DisplayName", "casdoorName":"DisplayName",
"isHashed":true, "isHashed":true,

View File

@@ -69,27 +69,35 @@ class SignupTable extends React.Component {
key: 'name', key: 'name',
render: (text, record, index) => { render: (text, record, index) => {
const items = [ const items = [
{id: 'Username', name: 'Username'}, {name: "Username", displayName: i18next.t("signup:Username")},
{id: 'ID', name: 'ID'}, {name: "ID", displayName: i18next.t("general:ID")},
{id: 'Display name', name: 'Display name'}, {name: "Display name", displayName: i18next.t("general:Display name")},
{id: 'Affiliation', name: 'Affiliation'}, {name: "Affiliation", displayName: i18next.t("user:Affiliation")},
{id: 'Country/Region', name: 'Country/Region'}, {name: "Country/Region", displayName: i18next.t("user:Country/Region")},
{id: 'ID card', name: 'ID card'}, {name: "ID card", displayName: i18next.t("user:ID card")},
{id: 'Email', name: 'Email'}, {name: "Email", displayName: i18next.t("general:Email")},
{id: 'Password', name: 'Password'}, {name: "Password", displayName: i18next.t("forget:Password")},
{id: 'Confirm password', name: 'Confirm password'}, {name: "Confirm password", displayName: i18next.t("forget:Confirm")},
{id: 'Phone', name: 'Phone'}, {name: "Phone", displayName: i18next.t("general:Phone")},
{id: 'Agreement', name: 'Agreement'}, {name: "Agreement", displayName: i18next.t("signup:Agreement")},
]; ];
const getItemDisplayName = (text) => {
const item = items.filter(item => item.name === text);
if (item.length === 0) {
return "";
}
return item[0].displayName;
};
return ( return (
<Select virtual={false} style={{width: '100%'}} <Select virtual={false} style={{width: '100%'}}
value={text} value={getItemDisplayName(text)}
onChange={value => { onChange={value => {
this.updateField(table, index, 'name', value); this.updateField(table, index, 'name', value);
}} > }} >
{ {
Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.name}</Option>) Setting.getDeduplicatedArray(items, table, "name").map((item, index) => <Option key={index} value={item.name}>{item.displayName}</Option>)
} }
</Select> </Select>
) )
@@ -156,10 +164,10 @@ class SignupTable extends React.Component {
} }
}, },
{ {
title: i18next.t("provider:rule"), title: i18next.t("application:rule"),
dataIndex: 'rule', dataIndex: 'rule',
key: 'rule', key: 'rule',
width: '120px', width: '155px',
render: (text, record, index) => { render: (text, record, index) => {
let options = []; let options = [];
if (record.name === "ID") { if (record.name === "ID") {
@@ -167,12 +175,17 @@ class SignupTable extends React.Component {
{id: 'Random', name: 'Random'}, {id: 'Random', name: 'Random'},
{id: 'Incremental', name: 'Incremental'}, {id: 'Incremental', name: 'Incremental'},
]; ];
} if (record.name === "Display name") { } else if (record.name === "Display name") {
options = [ options = [
{id: 'None', name: 'None'}, {id: 'None', name: 'None'},
{id: 'Real name', name: 'Real name'}, {id: 'Real name', name: 'Real name'},
{id: 'First, last', name: 'First, last'}, {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) { if (options.length === 0) {

View File

@@ -119,8 +119,12 @@ class SyncerEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => { <Select virtual={false} style={{width: '100%'}} value={this.state.syncer.type} onChange={(value => {
this.updateSyncerField('type', value); this.updateSyncerField('type', value);
this.state.syncer["tableColumns"] = Setting.getSyncerTableColumns(this.state.syncer); let syncer = this.state.syncer;
this.state.syncer.table = value === "Keycloak" ? "user_entity" : this.state.syncer.table; syncer["tableColumns"] = Setting.getSyncerTableColumns(this.state.syncer);
syncer.table = (value === "Keycloak") ? "user_entity" : this.state.syncer.table;
this.setState({
syncer: syncer,
});
})}> })}>
{ {
['Database', 'LDAP', 'Keycloak'] ['Database', 'LDAP', 'Keycloak']

View File

@@ -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) { renderTable(syncers) {
const columns = [ const columns = [
{ {
@@ -205,12 +219,13 @@ class SyncerListPage extends BaseListPage {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: '', dataIndex: '',
key: 'op', key: 'op',
width: '170px', width: '240px',
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <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 <Popconfirm
title={`Sure to delete syncer: ${record.name} ?`} title={`Sure to delete syncer: ${record.name} ?`}
onConfirm={() => this.deleteSyncer(index)} onConfirm={() => this.deleteSyncer(index)}

View File

@@ -19,7 +19,6 @@ import moment from "moment";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import * as TokenBackend from "./backend/TokenBackend"; import * as TokenBackend from "./backend/TokenBackend";
import i18next from "i18next"; import i18next from "i18next";
import * as ResourceBackend from "./backend/ResourceBackend";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
class TokenListPage extends BaseListPage { class TokenListPage extends BaseListPage {

View File

@@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import React from "react"; 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 UserBackend from "./backend/UserBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
@@ -47,6 +47,7 @@ class UserEditPage extends React.Component {
organizations: [], organizations: [],
applications: [], applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit", mode: props.location.mode !== undefined ? props.location.mode : "edit",
loading: true,
}; };
} }
@@ -59,9 +60,14 @@ class UserEditPage extends React.Component {
getUser() { getUser() {
UserBackend.getUser(this.state.organizationName, this.state.userName) 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({ this.setState({
user: user, loading: false,
}); });
}); });
} }
@@ -118,46 +124,86 @@ class UserEditPage extends React.Component {
return (this.state.user.id === this.props.account?.id) || Setting.isAdminUser(this.props.account); return (this.state.user.id === this.props.account?.id) || Setting.isAdminUser(this.props.account);
} }
renderUser() { renderAccountItem(accountItem) {
return ( if (!accountItem.visible) {
<Card size="small" title={ return null;
<div> }
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button> const isSelf = this.state.user.id === this.props.account?.id;
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button> const isAdmin = Setting.isAdminUser(this.props.account);
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div> // return (
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner"> // <div>
// {
// JSON.stringify({accountItem: accountItem, isSelf: isSelf, isAdmin: isAdmin})
// }
// </div>
// )
if (accountItem.viewRule === "Self") {
if (!isSelf && !isAdmin) {
return null;
}
} else if (accountItem.viewRule === "Admin") {
if (!isAdmin) {
return null;
}
}
let disabled = false;
if (accountItem.modifyRule === "Self") {
if (!isSelf && !isAdmin) {
disabled = true;
}
} else if (accountItem.modifyRule === "Admin") {
if (!isAdmin) {
disabled = true;
}
} else if (accountItem.modifyRule === "Immutable") {
disabled = true;
}
if (accountItem.name === "Organization") {
return (
<Row style={{marginTop: '10px'}} > <Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.user.owner} onChange={(value => {this.updateUserField('owner', value);})}> <Select virtual={false} style={{width: '100%'}} disabled={disabled} value={this.state.user.owner} onChange={(value => {this.updateUserField('owner', value);})}>
{ {
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>) this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "ID") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel("ID", i18next.t("general:ID - Tooltip"))} : {Setting.getLabel("ID", i18next.t("general:ID - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.user.id} disabled={true} /> <Input value={this.state.user.id} disabled={disabled} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Name") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.user.name} disabled={!Setting.isAdminUser(this.props.account)} onChange={e => { <Input value={this.state.user.name} disabled={disabled} onChange={e => {
this.updateUserField('name', e.target.value); this.updateUserField('name', e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Display name") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
@@ -168,6 +214,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Avatar") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} : {Setting.getLabel(i18next.t("general:Avatar"), i18next.t("general:Avatar - Tooltip"))} :
@@ -198,6 +247,9 @@ class UserEditPage extends React.Component {
</Row> </Row>
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "User type") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User type"), i18next.t("general:User type - Tooltip"))} : {Setting.getLabel(i18next.t("general:User type"), i18next.t("general:User type - Tooltip"))} :
@@ -211,44 +263,56 @@ class UserEditPage extends React.Component {
</Select> </Select>
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Password") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} : {Setting.getLabel(i18next.t("general:Password"), i18next.t("general:Password - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<PasswordModal user={this.state.user} account={this.props.account} disabled={this.state.userName !== this.state.user.name} /> <PasswordModal user={this.state.user} account={this.props.account} disabled={disabled} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Email") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} : {Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
</Col> </Col>
<Col style={{paddingRight: '20px'}} span={11} > <Col style={{paddingRight: '20px'}} span={11} >
<Input value={this.state.user.email} <Input value={this.state.user.email}
disabled={this.state.user.id === this.props.account?.id ? true : !Setting.isAdminUser(this.props.account)} disabled={disabled}
onChange={e => { onChange={e => {
this.updateUserField('email', e.target.value); this.updateUserField('email', e.target.value);
}} /> }} />
</Col> </Col>
<Col span={11} > <Col span={11} >
{ this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null} { this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Email...")} destType={"email"} />) : null}
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Phone") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} : {Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col> </Col>
<Col style={{paddingRight: '20px'}} span={11} > <Col style={{paddingRight: '20px'}} span={11} >
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`} <Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`}
disabled={this.state.user.id === this.props.account?.id ? true : !Setting.isAdminUser(this.props.account)} disabled={disabled}
onChange={e => { onChange={e => {
this.updateUserField('phone', e.target.value); this.updateUserField('phone', e.target.value);
}}/> }}/>
</Col> </Col>
<Col span={11} > <Col span={11} >
{ this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null} { this.state.user.id === this.props.account?.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Phone...")} destType={"phone"} />) : null}
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Country/Region") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Country/Region"), i18next.t("user:Country/Region - Tooltip"))} : {Setting.getLabel(i18next.t("user:Country/Region"), i18next.t("user:Country/Region - Tooltip"))} :
@@ -259,6 +323,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Location") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Location"), i18next.t("user:Location - Tooltip"))} : {Setting.getLabel(i18next.t("user:Location"), i18next.t("user:Location - Tooltip"))} :
@@ -269,11 +336,15 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
{ )
(this.state.application === null || this.state.user === null) ? null : ( } else if (accountItem.name === "Affiliation") {
<AffiliationSelect labelSpan={(Setting.isMobile()) ? 22 : 2} application={this.state.application} user={this.state.user} onUpdateUserField={(key, value) => { return this.updateUserField(key, value)}} /> return (
) (this.state.application === null || this.state.user === null) ? null : (
} <AffiliationSelect labelSpan={(Setting.isMobile()) ? 22 : 2} application={this.state.application} user={this.state.user} onUpdateUserField={(key, value) => { return this.updateUserField(key, value)}} />
)
)
} else if (accountItem.name === "Title") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} : {Setting.getLabel(i18next.t("user:Title"), i18next.t("user:Title - Tooltip"))} :
@@ -284,6 +355,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Homepage") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Homepage"), i18next.t("user:Homepage - Tooltip"))} : {Setting.getLabel(i18next.t("user:Homepage"), i18next.t("user:Homepage - Tooltip"))} :
@@ -294,6 +368,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Bio") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Bio"), i18next.t("user:Bio - Tooltip"))} : {Setting.getLabel(i18next.t("user:Bio"), i18next.t("user:Bio - Tooltip"))} :
@@ -304,6 +381,9 @@ class UserEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Tag") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} : {Setting.getLabel(i18next.t("user:Tag"), i18next.t("user:Tag - Tooltip"))} :
@@ -329,98 +409,136 @@ class UserEditPage extends React.Component {
} }
</Col> </Col>
</Row> </Row>
)
} else if (accountItem.name === "Signup application") {
return (
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Signup application"), i18next.t("general:Signup application - Tooltip"))} : {Setting.getLabel(i18next.t("general:Signup application"), i18next.t("general:Signup application - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: '100%'}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.user.signupApplication} onChange={(value => {this.updateUserField('signupApplication', value);})}> <Select virtual={false} style={{width: '100%'}} disabled={disabled} value={this.state.user.signupApplication} onChange={(value => {this.updateUserField('signupApplication', value);})}>
{ {
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>) this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
} }
</Select> </Select>
</Col> </Col>
</Row> </Row>
{ )
!this.isSelfOrAdmin() ? null : ( } else if (accountItem.name === "3rd-party logins") {
<Row style={{marginTop: '20px'}} > return (
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> !this.isSelfOrAdmin() ? null : (
{Setting.getLabel(i18next.t("user:3rd-party logins"), i18next.t("user:3rd-party logins - Tooltip"))} : <Row style={{marginTop: '20px'}} >
</Col> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
<Col span={22} > {Setting.getLabel(i18next.t("user:3rd-party logins"), i18next.t("user:3rd-party logins - Tooltip"))} :
<div style={{marginBottom: 20}}> </Col>
{ <Col span={22} >
(this.state.application === null || this.state.user === null) ? null : ( <div style={{marginBottom: 20}}>
this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) => {
(providerItem.provider.category === "OAuth") ? ( (this.state.application === null || this.state.user === null) ? null : (
<OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} /> this.state.application?.providers.filter(providerItem => Setting.isProviderVisible(providerItem)).map((providerItem, index) =>
) : ( (providerItem.provider.category === "OAuth") ? (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} /> <OAuthWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />
) ) : (
<SamlWidget key={providerItem.name} labelSpan={(Setting.isMobile()) ? 10 : 3} user={this.state.user} application={this.state.application} providerItem={providerItem} onUnlinked={() => { return this.unlinked()}} />
) )
) )
} )
</div> }
</Col> </div>
</Row> </Col>
) </Row>
} )
)
} else if (accountItem.name === "Properties") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("user:Properties")}:
</Col>
<Col span={22} >
<CodeMirror
value={JSON.stringify(this.state.user.properties, null, 4)}
options={{mode: 'javascript', theme: "material-darker"}}
/>
</Col>
</Row>
)
} else if (accountItem.name === "Is admin") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is admin"), i18next.t("user:Is admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isAdmin} onChange={checked => {
this.updateUserField('isAdmin', checked);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Is global admin") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is global admin"), i18next.t("user:Is global admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isGlobalAdmin} onChange={checked => {
this.updateUserField('isGlobalAdmin', checked);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Is forbidden") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is forbidden"), i18next.t("user:Is forbidden - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isForbidden} onChange={checked => {
this.updateUserField('isForbidden', checked);
}} />
</Col>
</Row>
)
} else if (accountItem.name === "Is deleted") {
return (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is deleted"), i18next.t("user:Is deleted - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isDeleted} onChange={checked => {
this.updateUserField('isDeleted', checked);
}} />
</Col>
</Row>
)
}
}
renderUser() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("user:New User") : i18next.t("user:Edit User")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitUserEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: '20px'}} type="primary" onClick={() => this.submitUserEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: '20px'}} onClick={() => this.deleteUser()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
{ {
!Setting.isAdminUser(this.props.account) ? null : ( this.state.application?.organizationObj.accountItems?.map(accountItem => {
<React.Fragment> return (
{/*<Row style={{marginTop: '20px'}} >*/} <React.Fragment key={accountItem.name}>
{/* <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>*/} {
{/* {i18next.t("user:Properties")}:*/} this.renderAccountItem(accountItem)
{/* </Col>*/} }
{/* <Col span={22} >*/} </React.Fragment>
{/* <CodeMirror*/} )
{/* value={JSON.stringify(this.state.user.properties, null, 4)}*/} })
{/* options={{mode: 'javascript', theme: "material-darker"}}*/}
{/* />*/}
{/* </Col>*/}
{/*</Row>*/}
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is admin"), i18next.t("user:Is admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isAdmin} onChange={checked => {
this.updateUserField('isAdmin', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is global admin"), i18next.t("user:Is global admin - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isGlobalAdmin} onChange={checked => {
this.updateUserField('isGlobalAdmin', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is forbidden"), i18next.t("user:Is forbidden - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isForbidden} onChange={checked => {
this.updateUserField('isForbidden', checked);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:Is deleted"), i18next.t("user:Is deleted - Tooltip"))} :
</Col>
<Col span={(Setting.isMobile()) ? 22 : 2} >
<Switch checked={this.state.user.isDeleted} onChange={checked => {
this.updateUserField('isDeleted', checked);
}} />
</Col>
</Row>
</React.Fragment>
)
} }
</Card> </Card>
) )
@@ -469,13 +587,24 @@ class UserEditPage extends React.Component {
return ( return (
<div> <div>
{ {
this.state.user !== null ? this.renderUser() : null this.state.loading ? <Spin 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> </div>
); );
} }

Some files were not shown because too many files have changed in this diff Show More