Compare commits

...

48 Commits

Author SHA1 Message Date
3373174c65 fix: add missing tableNamePrefix in some places 2023-12-31 22:40:41 +08:00
2fb79e4092 Add invitation pages 2023-12-31 21:38:36 +08:00
5846e337c7 feat: fix gofmt issue 2023-12-30 19:47:04 +08:00
44f4de1440 feat: support empty fileUrl in GetUploadFileUrl() 2023-12-30 19:06:35 +08:00
27adeb4620 Refactor initAPI() 2023-12-30 14:28:45 +08:00
5c107db43b fix: fix i18n typo 2023-12-30 00:49:39 +08:00
27187b3a54 feat: add "Reset to Default HTML" button 2023-12-30 00:47:10 +08:00
14fcedcc5d feat: support HTML in Email content 2023-12-29 23:31:50 +08:00
e7c015f288 feat: fix comment and configs for successfully generating OpenAPI typescript-axios sdk (#2560)
* fix: fix swagger.json, successfully generate java sdk

* fix:fix comment and change some content for successfully generating typescript-axios sdk
2023-12-29 15:12:40 +08:00
c4819602ec fix: add mfa API to isAllowedInDemoMode() 2023-12-26 20:06:27 +08:00
dea03cdd15 feat: replace deprecated github.com/RobotsAndPencils/go-saml (#2558)
The `github.com/RobotsAndPencils/go-saml` has been officially deprecated
and archived on 7 June 2023.

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2023-12-25 21:15:53 +08:00
21f394847e fix: fix Swagger docs by improving comments 2023-12-23 11:57:18 +08:00
9bef9691fb feat: fix volcengine SMS provider error handling 2023-12-22 20:49:45 +08:00
141f22a707 feat: upgrade to Node.js 18 and Go 1.20 in Dockerfile 2023-12-22 14:46:41 +08:00
02329d342a feat: fix bug in "*" users and roles in permission edit page. 2023-12-22 14:16:00 +08:00
b9d3e2184c fix: update CI node version from 16 to 18 2023-12-22 09:28:45 +08:00
28caf8550e Support token parsed result 2023-12-22 02:04:25 +08:00
79159dc809 Improve TokenEditPage 2023-12-22 00:44:34 +08:00
63081641d6 Improve i18n text 2023-12-22 00:25:46 +08:00
698f24f762 feat: fix template code bug in SMS provider of Amazon SNS 2023-12-21 23:32:55 +08:00
5499e62d7f feat: add the FailedSigninLimit and FailedSigninfrozenTime configuration options to the application (#2552)
Add configuration items to the application to limit the number of logins and the login wait time after the maximum number of errors is reached
feat: #2272

fix: fixed the issue where the token parameter could be set to a negative value
2023-12-20 22:29:53 +08:00
f8905ae64c Fix S3-compliant storage providers support 2023-12-20 14:38:32 +08:00
a42594859f feat: improve enforce() and batchEnforce() API response 2023-12-20 11:41:54 +08:00
46e0bc1a39 Improve i18n texts 2023-12-20 10:09:00 +08:00
ffe2330238 Fix tag field in user list page 2023-12-20 01:57:56 +08:00
ec53616dc8 Update README.md 2023-12-20 01:52:29 +08:00
067276d739 Add new B2C provider 2023-12-17 16:29:29 +08:00
468ceb6b71 Fix get-all-objects API 403 issue 2023-12-15 21:32:45 +08:00
b31a317585 feat: add helm release github action (#2546) 2023-12-15 19:30:10 +08:00
396b6fb65f feat: refactor custom HTTP related filenames 2023-12-15 00:06:05 +08:00
be637fca81 fix: fix wrong POST param logic in custom HTTP providers 2023-12-15 00:00:47 +08:00
374928e719 feat: add custom HTTP Email provider (#2542)
* feat: implement Custom HTTP Email provider

* Update Setting.js

* Update ProviderEditPage.js

* Update http.go

* Update provider.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-12-14 22:35:25 +08:00
5c103e8cd3 Improve error handling in GenerateIdForNewUser() 2023-12-14 10:12:00 +08:00
85b86e8831 fix: dropped object group errors (#2545) 2023-12-14 09:00:25 +08:00
08864686f3 feat: fix Google cloud storage provider bug 2023-12-14 00:25:50 +08:00
dc06eb9948 feat: fix secret information issue in the CAPTCHA provider code (#2531) 2023-12-11 18:01:56 +08:00
b068202e74 Improve Radius username handling 2023-12-11 18:01:28 +08:00
cb16567c7b feat: helm support extra containers (#2530) 2023-12-10 14:41:56 +08:00
4eb725d47a Improve image upload UI 2023-12-08 19:42:20 +08:00
ce72a172b0 feat: add back Custom HTTP SMS provider 2023-12-07 16:59:41 +08:00
5521962e0c feat: update go-sms-sender to v0.17.0 to improve error handling 2023-12-07 14:25:21 +08:00
37b8b09cc0 feat: update go-sms-sender to v0.16.0 to fix first number missing bug in AmazonSNSClient.SendMessage 2023-12-06 20:05:48 +08:00
482eb61168 feat: improve StaticFilter() 2023-12-05 18:33:06 +08:00
8819a8697b feat: fix dropped error in stripe.go (#2525) 2023-12-05 16:02:33 +08:00
85cb68eb66 feat: unbind LDAP clients if not used any more 2023-12-02 17:51:25 +08:00
b25b5f0249 Support original accessToken in token APIs 2023-12-02 16:56:18 +08:00
947dcf6e75 Fix "All" roles bug in permission edit page 2023-12-02 15:26:52 +08:00
113c27db73 Improve logout's id_token_hint logic 2023-12-02 02:13:34 +08:00
137 changed files with 5982 additions and 1950 deletions

View File

@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'yarn'
cache-dependency-path: ./web/yarn.lock
- run: yarn install && CI=false yarn run build
@ -101,7 +101,7 @@ jobs:
working-directory: ./
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'yarn'
cache-dependency-path: ./web/yarn.lock
- run: yarn install
@ -137,7 +137,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Fetch Previous version
id: get-previous-tag

40
.github/workflows/helm.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Helm Release
on:
push:
branches:
- master
paths:
- 'manifests/casdoor/Chart.yaml'
jobs:
release-helm-chart:
name: Release Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Release Helm Chart
run: |
cd manifests/casdoor
REGISTRY=oci://registry-1.docker.io/casbin
helm package .
PKG_NAME=$(ls *.tgz)
helm repo index . --url $REGISTRY --merge index.yaml
helm push $PKG_NAME $REGISTRY
rm $PKG_NAME
- name: Commit updated helm index.yaml
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'ci: update helm index.yaml'

View File

@ -1,10 +1,10 @@
FROM node:16.18.0 AS FRONT
FROM node:18.19.0 AS FRONT
WORKDIR /web
COPY ./web .
RUN yarn install --frozen-lockfile --network-timeout 1000000 && yarn run build
FROM golang:1.19.9 AS BACK
FROM golang:1.20.12 AS BACK
WORKDIR /go/src/casdoor
COPY . .
RUN ./build.sh

View File

@ -42,6 +42,20 @@
</a>
</p>
<p align="center">
<sup>Sponsored by</sup>
<br>
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.casbin.org/img/stytch-white.png">
<source media="(prefers-color-scheme: light)" srcset="https://cdn.casbin.org/img/stytch-charcoal.png">
<img src="https://cdn.casbin.org/img/stytch-charcoal.png" width="275">
</picture>
</a><br/>
<a href="https://stytch.com/docs?utm_source=oss-sponsorship&utm_medium=paid_sponsorship&utm_campaign=casbin"><b>Build auth with fraud prevention, faster.</b><br/> Try Stytch for API-first authentication, user & org management, multi-tenant SSO, MFA, device fingerprinting, and more.</a>
<br>
</p>
## Online demo
- Read-only site: https://door.casdoor.com (any modification operation will fail)

View File

@ -92,6 +92,9 @@ p, *, *, GET, /api/get-plan, *, *
p, *, *, GET, /api/get-subscription, *, *
p, *, *, GET, /api/get-provider, *, *
p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, *
`
sa := stringadapter.NewAdapter(ruleText)
@ -147,7 +150,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

@ -56,6 +56,17 @@ type Captcha struct {
SubType string `json:"subType"`
}
// this API is used by "Api URL" of Flarum's FoF Passport plugin
// https://github.com/FriendsOfFlarum/passport
type LaravelResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
EmailVerifiedAt string `json:"email_verified_at"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// Signup
// @Tag Login API
// @Title Signup
@ -238,7 +249,7 @@ func (c *ApiController) Signup() {
// @Param post_logout_redirect_uri query string false "post_logout_redirect_uri"
// @Param state query string false "state"
// @Success 200 {object} controllers.Response The Response object
// @router /logout [get,post]
// @router /logout [post]
func (c *ApiController) Logout() {
// https://openid.net/specs/openid-connect-rpinitiated-1_0-final.html
accessToken := c.Input().Get("id_token_hint")
@ -282,17 +293,15 @@ func (c *ApiController) Logout() {
return
}
affected, application, token, err := object.ExpireTokenByAccessToken(accessToken)
_, application, token, err := object.ExpireTokenByAccessToken(accessToken)
if err != nil {
c.ResponseError(err.Error())
return
}
if !affected {
if token == nil {
c.ResponseError(c.T("token:Token not found, invalid accessToken"))
return
}
if application == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist")), token.Application)
return
@ -319,7 +328,15 @@ func (c *ApiController) Logout() {
return
} else {
if application.IsRedirectUriValid(redirectUri) {
c.Ctx.Redirect(http.StatusFound, fmt.Sprintf("%s?state=%s", strings.TrimRight(redirectUri, "/"), state))
redirectUrl := redirectUri
if state != "" {
if strings.Contains(redirectUri, "?") {
redirectUrl = fmt.Sprintf("%s&state=%s", strings.TrimSuffix(redirectUri, "/"), state)
} else {
redirectUrl = fmt.Sprintf("%s?state=%s", strings.TrimSuffix(redirectUri, "/"), state)
}
}
c.Ctx.Redirect(http.StatusFound, redirectUrl)
} else {
c.ResponseError(fmt.Sprintf(c.T("token:Redirect URI: %s doesn't exist in the allowed Redirect URI list"), redirectUri))
return
@ -412,7 +429,7 @@ func (c *ApiController) GetUserinfo() {
// @Title UserInfo2
// @Tag Account API
// @Description return Laravel compatible user information according to OAuth 2.0
// @Success 200 {object} LaravelResponse The Response object
// @Success 200 {object} controllers.LaravelResponse The Response object
// @router /user [get]
func (c *ApiController) GetUserinfo2() {
user, ok := c.RequireSignedInUser()
@ -420,17 +437,6 @@ func (c *ApiController) GetUserinfo2() {
return
}
// this API is used by "Api URL" of Flarum's FoF Passport plugin
// https://github.com/FriendsOfFlarum/passport
type LaravelResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
EmailVerifiedAt string `json:"email_verified_at"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
response := LaravelResponse{
Id: user.Id,
Name: user.Name,
@ -448,6 +454,7 @@ func (c *ApiController) GetUserinfo2() {
// @Tag Login API
// @Title GetCaptcha
// @router /api/get-captcha [get]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) GetCaptcha() {
applicationId := c.Input().Get("applicationId")
isCurrentProvider := c.Input().Get("isCurrentProvider")
@ -473,7 +480,7 @@ func (c *ApiController) GetCaptcha() {
Type: captchaProvider.Type,
SubType: captchaProvider.SubType,
ClientId: captchaProvider.ClientId,
ClientSecret: captchaProvider.ClientSecret,
ClientSecret: "***",
ClientId2: captchaProvider.ClientId2,
ClientSecret2: captchaProvider.ClientSecret2,
})

View File

@ -110,6 +110,14 @@ func (c *ApiController) GetApplication() {
}
}
// 0 as an initialization value, corresponding to the default configuration parameters
if application.FailedSigninLimit == 0 {
application.FailedSigninLimit = object.DefaultFailedSigninLimit
}
if application.FailedSigninfrozenTime == 0 {
application.FailedSigninfrozenTime = object.DefaultFailedSigninfrozenTime
}
c.ResponseOk(object.GetMaskedApplication(application, userId))
}

View File

@ -222,7 +222,7 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
// @Param redirectUri query string true "redirect uri"
// @Param scope query string true "scope"
// @Param state query string true "state"
// @Success 200 {object} Response The Response object
// @Success 200 {object} controllers.Response The Response object
// @router /get-app-login [get]
func (c *ApiController) GetApplicationLogin() {
clientId := c.Input().Get("clientId")
@ -387,6 +387,16 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
} else if enableCaptcha {
captchaProvider, err := object.GetCaptchaProviderByApplication(util.GetId(application.Owner, application.Name), "false", c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if captchaProvider.Type != "Default" {
authForm.ClientSecret = captchaProvider.ClientSecret
}
var isHuman bool
isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret)
if err != nil {
@ -877,6 +887,7 @@ func (c *ApiController) HandleSamlLogin() {
// @Tag HandleOfficialAccountEvent API
// @Title HandleOfficialAccountEvent
// @router /api/webhook [POST]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) HandleOfficialAccountEvent() {
respBytes, err := ioutil.ReadAll(c.Ctx.Request.Body)
if err != nil {
@ -907,6 +918,7 @@ func (c *ApiController) HandleOfficialAccountEvent() {
// @Tag GetWebhookEventType API
// @Title GetWebhookEventType
// @router /api/get-webhook-event [GET]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) GetWebhookEventType() {
lock.Lock()
defer lock.Unlock()
@ -936,8 +948,14 @@ func (c *ApiController) GetCaptchaStatus() {
return
}
failedSigninLimit, _, err := object.GetFailedSigninConfigByUser(user)
if err != nil {
c.ResponseError(err.Error())
return
}
var captchaEnabled bool
if user != nil && user.SigninWrongTimes >= object.SigninWrongTimesLimit {
if user != nil && user.SigninWrongTimes >= failedSigninLimit {
captchaEnabled = true
}
c.ResponseOk(captchaEnabled)
@ -948,6 +966,7 @@ func (c *ApiController) GetCaptchaStatus() {
// @Tag Callback API
// @Description Get Login Error Counts
// @router /api/Callback [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) Callback() {
code := c.GetString("code")
state := c.GetString("state")

View File

@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"fmt"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
@ -25,7 +26,7 @@ import (
// @Title Enforce
// @Tag Enforce API
// @Description Call Casbin Enforce API
// @Param body body object.CasbinRequest true "Casbin request"
// @Param body body []string true "Casbin request"
// @Param permissionId query string false "permission id"
// @Param modelId query string false "model id"
// @Param resourceId query string false "resource id"
@ -42,7 +43,7 @@ func (c *ApiController) Enforce() {
return
}
var request object.CasbinRequest
var request []string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &request)
if err != nil {
c.ResponseError(err.Error())
@ -56,13 +57,22 @@ func (c *ApiController) Enforce() {
return
}
res, err := enforcer.Enforce(request...)
res := []bool{}
keyRes := []string{}
// type transformation
interfaceRequest := util.StringToInterfaceArray(request)
enforceResult, err := enforcer.Enforce(interfaceRequest...)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(res)
res = append(res, enforceResult)
keyRes = append(keyRes, enforcer.GetModelAndAdapter())
c.ResponseOk(res, keyRes)
return
}
@ -72,22 +82,24 @@ func (c *ApiController) Enforce() {
c.ResponseError(err.Error())
return
}
res := []bool{}
if permission == nil {
res = append(res, false)
} else {
enforceResult, err := object.Enforce(permission, &request)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
c.ResponseError(fmt.Sprintf("permission: %s doesn't exist", permissionId))
return
}
c.ResponseOk(res)
res := []bool{}
keyRes := []string{}
enforceResult, err := object.Enforce(permission, request)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
keyRes = append(keyRes, permission.GetModelAndAdapter())
c.ResponseOk(res, keyRes)
return
}
@ -111,32 +123,33 @@ func (c *ApiController) Enforce() {
}
res := []bool{}
keyRes := []string{}
listPermissionIdMap := object.GroupPermissionsByModelAdapter(permissions)
for _, permissionIds := range listPermissionIdMap {
for key, permissionIds := range listPermissionIdMap {
firstPermission, err := object.GetPermission(permissionIds[0])
if err != nil {
c.ResponseError(err.Error())
return
}
enforceResult, err := object.Enforce(firstPermission, &request, permissionIds...)
enforceResult, err := object.Enforce(firstPermission, request, permissionIds...)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
keyRes = append(keyRes, key)
}
c.ResponseOk(res)
c.ResponseOk(res, keyRes)
}
// BatchEnforce
// @Title BatchEnforce
// @Tag Enforce API
// @Description Call Casbin BatchEnforce API
// @Param body body object.CasbinRequest true "array of casbin requests"
// @Param body body []string true "array of casbin requests"
// @Param permissionId query string false "permission id"
// @Param modelId query string false "model id"
// @Success 200 {object} controllers.Response The Response object
@ -146,7 +159,7 @@ func (c *ApiController) BatchEnforce() {
modelId := c.Input().Get("modelId")
enforcerId := c.Input().Get("enforcerId")
var requests []object.CasbinRequest
var requests [][]string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &requests)
if err != nil {
c.ResponseError(err.Error())
@ -160,13 +173,22 @@ func (c *ApiController) BatchEnforce() {
return
}
res, err := enforcer.BatchEnforce(requests)
res := [][]bool{}
keyRes := []string{}
// type transformation
interfaceRequests := util.StringToInterfaceArray2d(requests)
enforceResult, err := enforcer.BatchEnforce(interfaceRequests)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(res)
res = append(res, enforceResult)
keyRes = append(keyRes, enforcer.GetModelAndAdapter())
c.ResponseOk(res, keyRes)
return
}
@ -176,28 +198,24 @@ func (c *ApiController) BatchEnforce() {
c.ResponseError(err.Error())
return
}
res := [][]bool{}
if permission == nil {
l := len(requests)
resRequest := make([]bool, l)
for i := 0; i < l; i++ {
resRequest[i] = false
}
res = append(res, resRequest)
} else {
enforceResult, err := object.BatchEnforce(permission, &requests)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
c.ResponseError(fmt.Sprintf("permission: %s doesn't exist", permissionId))
return
}
c.ResponseOk(res)
res := [][]bool{}
keyRes := []string{}
enforceResult, err := object.BatchEnforce(permission, requests)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
keyRes = append(keyRes, permission.GetModelAndAdapter())
c.ResponseOk(res, keyRes)
return
}
@ -215,7 +233,7 @@ func (c *ApiController) BatchEnforce() {
}
res := [][]bool{}
keyRes := []string{}
listPermissionIdMap := object.GroupPermissionsByModelAdapter(permissions)
for _, permissionIds := range listPermissionIdMap {
firstPermission, err := object.GetPermission(permissionIds[0])
@ -224,16 +242,17 @@ func (c *ApiController) BatchEnforce() {
return
}
enforceResult, err := object.BatchEnforce(firstPermission, &requests, permissionIds...)
enforceResult, err := object.BatchEnforce(firstPermission, requests, permissionIds...)
if err != nil {
c.ResponseError(err.Error())
return
}
res = append(res, enforceResult)
keyRes = append(keyRes, firstPermission.GetModelAndAdapter())
}
c.ResponseOk(res)
c.ResponseOk(res, keyRes)
}
func (c *ApiController) GetAllObjects() {
@ -243,7 +262,13 @@ func (c *ApiController) GetAllObjects() {
return
}
c.ResponseOk(object.GetAllObjects(userId))
objects, err := object.GetAllObjects(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(objects)
}
func (c *ApiController) GetAllActions() {
@ -253,7 +278,13 @@ func (c *ApiController) GetAllActions() {
return
}
c.ResponseOk(object.GetAllActions(userId))
actions, err := object.GetAllActions(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(actions)
}
func (c *ApiController) GetAllRoles() {
@ -263,5 +294,11 @@ func (c *ApiController) GetAllRoles() {
return
}
c.ResponseOk(object.GetAllRoles(userId))
roles, err := object.GetAllRoles(userId)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(roles)
}

View File

@ -71,7 +71,7 @@ func (c *ApiController) GetEnforcers() {
// @Tag Enforcer API
// @Description get enforcer
// @Param id query string true "The id ( owner/name ) of enforcer"
// @Success 200 {object} object
// @Success 200 {object} object.Enforcer
// @router /get-enforcer [get]
func (c *ApiController) GetEnforcer() {
id := c.Input().Get("id")
@ -99,7 +99,7 @@ func (c *ApiController) GetEnforcer() {
// @Description update enforcer
// @Param id query string true "The id ( owner/name ) of enforcer"
// @Param enforcer body object true "The enforcer object"
// @Success 200 {object} object
// @Success 200 {object} object.Enforcer
// @router /update-enforcer [post]
func (c *ApiController) UpdateEnforcer() {
id := c.Input().Get("id")
@ -120,7 +120,7 @@ func (c *ApiController) UpdateEnforcer() {
// @Tag Enforcer API
// @Description add enforcer
// @Param enforcer body object true "The enforcer object"
// @Success 200 {object} object
// @Success 200 {object} object.Enforcer
// @router /add-enforcer [post]
func (c *ApiController) AddEnforcer() {
enforcer := object.Enforcer{}
@ -138,8 +138,8 @@ func (c *ApiController) AddEnforcer() {
// @Title DeleteEnforcer
// @Tag Enforcer API
// @Description delete enforcer
// @Param body body object.Enforce true "The enforcer object"
// @Success 200 {object} object
// @Param body body object.Enforcer true "The enforcer object"
// @Success 200 {object} object.Enforcer
// @router /delete-enforcer [post]
func (c *ApiController) DeleteEnforcer() {
var enforcer object.Enforcer

164
controllers/invitation.go Normal file
View File

@ -0,0 +1,164 @@
// Copyright 2023 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/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetInvitations
// @Title GetInvitations
// @Tag Invitation API
// @Description get invitations
// @Param owner query string true "The owner of invitations"
// @Success 200 {array} object.Invitation The Response object
// @router /get-invitations [get]
func (c *ApiController) GetInvitations() {
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 == "" {
invitations, err := object.GetInvitations(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(invitations)
} else {
limit := util.ParseInt(limit)
count, err := object.GetInvitationCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limit, count)
invitations, err := object.GetPaginationInvitations(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(invitations, paginator.Nums())
}
}
// GetInvitation
// @Title GetInvitation
// @Tag Invitation API
// @Description get invitation
// @Param id query string true "The id ( owner/name ) of the invitation"
// @Success 200 {object} object.Invitation The Response object
// @router /get-invitation [get]
func (c *ApiController) GetInvitation() {
id := c.Input().Get("id")
invitation, err := object.GetInvitation(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(invitation)
}
// UpdateInvitation
// @Title UpdateInvitation
// @Tag Invitation API
// @Description update invitation
// @Param id query string true "The id ( owner/name ) of the invitation"
// @Param body body object.Invitation true "The details of the invitation"
// @Success 200 {object} controllers.Response The Response object
// @router /update-invitation [post]
func (c *ApiController) UpdateInvitation() {
id := c.Input().Get("id")
var invitation object.Invitation
err := json.Unmarshal(c.Ctx.Input.RequestBody, &invitation)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation))
c.ServeJSON()
}
// AddInvitation
// @Title AddInvitation
// @Tag Invitation API
// @Description add invitation
// @Param body body object.Invitation true "The details of the invitation"
// @Success 200 {object} controllers.Response The Response object
// @router /add-invitation [post]
func (c *ApiController) AddInvitation() {
var invitation object.Invitation
err := json.Unmarshal(c.Ctx.Input.RequestBody, &invitation)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation))
c.ServeJSON()
}
// DeleteInvitation
// @Title DeleteInvitation
// @Tag Invitation API
// @Description delete invitation
// @Param body body object.Invitation true "The details of the invitation"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-invitation [post]
func (c *ApiController) DeleteInvitation() {
var invitation object.Invitation
err := json.Unmarshal(c.Ctx.Input.RequestBody, &invitation)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteInvitation(&invitation))
c.ServeJSON()
}
// VerifyInvitation
// @Title VerifyInvitation
// @Tag Invitation API
// @Description verify invitation
// @Param id query string true "The id ( owner/name ) of the invitation"
// @Success 200 {object} controllers.Response The Response object
// @router /verify-invitation [get]
func (c *ApiController) VerifyInvitation() {
id := c.Input().Get("id")
payment, attachInfo, err := object.VerifyInvitation(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(payment, attachInfo)
}

View File

@ -42,7 +42,7 @@ type LdapSyncResp struct {
// @Tag Account API
// @Description get ldap users
// Param id string true "id"
// @Success 200 {object} LdapResp The Response object
// @Success 200 {object} controllers.LdapResp The Response object
// @router /get-ldap-users [get]
func (c *ApiController) GetLdapUsers() {
id := c.Input().Get("id")
@ -59,6 +59,7 @@ func (c *ApiController) GetLdapUsers() {
c.ResponseError(err.Error())
return
}
defer conn.Close()
//groupsMap, err := conn.GetLdapGroups(ldapServer.BaseDn)
//if err != nil {
@ -249,7 +250,7 @@ func (c *ApiController) DeleteLdap() {
// @Tag Account API
// @Description sync ldap users
// @Param id query string true "id"
// @Success 200 {object} LdapSyncResp The Response object
// @Success 200 {object} controllers.LdapSyncResp The Response object
// @router /sync-ldap-users [post]
func (c *ApiController) SyncLdapUsers() {
id := c.Input().Get("id")

View File

@ -26,8 +26,10 @@ type LinkForm struct {
}
// Unlink ...
// @router /unlink [post]
// @Tag Login API
// @Title Unlink
// @router /unlink [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) Unlink() {
user, ok := c.RequireSignedInUser()
if !ok {

View File

@ -73,7 +73,7 @@ func (c *ApiController) MfaSetupInitiate() {
// @Description setup verify totp
// @param secret form string true "MFA secret"
// @param passcode form string true "MFA passcode"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /mfa/setup/verify [post]
func (c *ApiController) MfaSetupVerify() {
mfaType := c.Ctx.Request.Form.Get("mfaType")
@ -104,7 +104,7 @@ func (c *ApiController) MfaSetupVerify() {
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param type form string true "MFA auth type"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /mfa/setup/enable [post]
func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner")
@ -143,7 +143,7 @@ func (c *ApiController) MfaSetupEnable() {
// @Description: Delete MFA
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /delete-mfa/ [post]
func (c *ApiController) DeleteMfa() {
owner := c.Ctx.Request.Form.Get("owner")
@ -176,7 +176,7 @@ func (c *ApiController) DeleteMfa() {
// @param owner form string true "owner of user"
// @param name form string true "name of user"
// @param id form string true "id of user's MFA props"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /set-preferred-mfa [post]
func (c *ApiController) SetPreferredMfa() {
mfaType := c.Ctx.Request.Form.Get("mfaType")

View File

@ -178,7 +178,7 @@ func (c *ApiController) DeleteOrganization() {
// @Tag Organization API
// @Description get default application
// @Param id query string true "organization id"
// @Success 200 {object} Response The Response object
// @Success 200 {object} controllers.Response The Response object
// @router /get-default-application [get]
func (c *ApiController) GetDefaultApplication() {
userId := c.GetSessionUsername()

View File

@ -51,9 +51,14 @@ type NotificationForm struct {
// @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application"
// @Param from body controllers.EmailForm true "Details of the email request"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /api/send-email [post]
func (c *ApiController) SendEmail() {
user, ok := c.RequireSignedInUser()
if !ok {
return
}
var emailForm EmailForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &emailForm)
@ -108,8 +113,13 @@ func (c *ApiController) SendEmail() {
}
code := "123456"
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(emailForm.Content, code)
content := strings.Replace(provider.Content, "%s", code, 1)
if user != nil {
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1)
}
for _, receiver := range emailForm.Receivers {
err = object.SendEmail(provider, emailForm.Title, content, receiver, emailForm.Sender)
if err != nil {
@ -128,7 +138,7 @@ func (c *ApiController) SendEmail() {
// @Param clientId query string true "The clientId of the application"
// @Param clientSecret query string true "The clientSecret of the application"
// @Param from body controllers.SmsForm true "Details of the sms request"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /api/send-sms [post]
func (c *ApiController) SendSms() {
provider, err := c.GetProviderFromContext("SMS")
@ -166,7 +176,7 @@ func (c *ApiController) SendSms() {
// @Tag Service API
// @Description This API is not for Casdoor frontend to call, it is for Casdoor SDKs.
// @Param from body controllers.NotificationForm true "Details of the notification request"
// @Success 200 {object} Response object
// @Success 200 {object} controllers.Response The Response object
// @router /api/send-notification [post]
func (c *ApiController) SendNotification() {
provider, err := c.GetProviderFromContext("Notification")

View File

@ -510,6 +510,7 @@ func (c *ApiController) SetPassword() {
// @Title CheckUserPassword
// @router /check-user-password [post]
// @Tag User API
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) CheckUserPassword() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
@ -580,6 +581,7 @@ func (c *ApiController) GetUserCount() {
// @Title AddUserKeys
// @router /add-user-keys [post]
// @Tag User API
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) AddUserKeys() {
var user object.User
err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)

View File

@ -39,6 +39,7 @@ const (
// @Title SendVerificationCode
// @Tag Verification API
// @router /send-verification-code [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) SendVerificationCode() {
var vform form.VerificationForm
err := c.ParseForm(&vform)
@ -53,17 +54,34 @@ func (c *ApiController) SendVerificationCode() {
return
}
if vform.CaptchaType != "none" {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
provider, err := object.GetCaptchaProviderByApplication(vform.ApplicationId, "false", c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if provider != nil {
if vform.CaptchaType != provider.Type {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
if provider.Type != "Default" {
vform.ClientSecret = provider.ClientSecret
}
if vform.CaptchaType != "none" {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return
} else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil {
c.ResponseError(err.Error())
return
} else if !isHuman {
c.ResponseError(c.T("verification:Turing test failed."))
return
}
}
}
application, err := object.GetApplication(vform.ApplicationId)
@ -212,6 +230,7 @@ func (c *ApiController) SendVerificationCode() {
// @Title VerifyCaptcha
// @Tag Verification API
// @router /verify-captcha [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) VerifyCaptcha() {
var vform form.VerificationForm
err := c.ParseForm(&vform)
@ -225,6 +244,16 @@ func (c *ApiController) VerifyCaptcha() {
return
}
captchaProvider, err := object.GetCaptchaProviderByOwnerName(vform.ApplicationId, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if captchaProvider.Type != "Default" {
vform.ClientSecret = captchaProvider.ClientSecret
}
provider := captcha.GetCaptchaProvider(vform.CaptchaType)
if provider == nil {
c.ResponseError(c.T("verification:Invalid captcha provider."))
@ -244,6 +273,7 @@ func (c *ApiController) VerifyCaptcha() {
// @Tag Account API
// @Title ResetEmailOrPhone
// @router /api/reset-email-or-phone [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) ResetEmailOrPhone() {
user, ok := c.RequireSignedInUser()
if !ok {
@ -338,6 +368,7 @@ func (c *ApiController) ResetEmailOrPhone() {
// @Tag Verification API
// @Title VerifyCode
// @router /api/verify-code [post]
// @Success 200 {object} object.Userinfo The Response object
func (c *ApiController) VerifyCode() {
var authForm form.AuthForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &authForm)

View File

@ -146,7 +146,7 @@ func (c *ApiController) WebAuthnSigninBegin() {
}
// WebAuthnSigninFinish
// @Title WebAuthnSigninBegin
// @Title WebAuthnSigninFinish
// @Tag Login API
// @Description WebAuthn Login Flow 2nd stage
// @Param body body protocol.CredentialAssertionResponse true "authenticator assertion Response"

82
email/custom_http.go Normal file
View File

@ -0,0 +1,82 @@
// Copyright 2023 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 email
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/casdoor/casdoor/proxy"
)
type HttpEmailProvider struct {
endpoint string
method string
}
func NewHttpEmailProvider(endpoint string, method string) *HttpEmailProvider {
client := &HttpEmailProvider{
endpoint: endpoint,
method: method,
}
return client
}
func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
var req *http.Request
var err error
if c.method == "POST" {
formValues := url.Values{}
formValues.Set("fromName", fromName)
formValues.Set("toAddress", toAddress)
formValues.Set("subject", subject)
formValues.Set("content", content)
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else if c.method == "GET" {
req, err = http.NewRequest(c.method, c.endpoint, nil)
if err != nil {
return err
}
q := req.URL.Query()
q.Add("fromName", fromName)
q.Add("toAddress", toAddress)
q.Add("subject", subject)
q.Add("content", content)
req.URL.RawQuery = q.Encode()
} else {
return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method)
}
httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HttpEmailProvider's Send() error, custom HTTP Email request failed with status: %s", resp.Status)
}
return err
}

View File

@ -18,9 +18,11 @@ type EmailProvider interface {
Send(fromAddress string, fromName, toAddress string, subject string, content string) error
}
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool) EmailProvider {
func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string) EmailProvider {
if typ == "Azure ACS" {
return NewAzureACSEmailProvider(clientSecret, host)
} else if typ == "Custom HTTP Email" {
return NewHttpEmailProvider(endpoint, method)
} else {
return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
}

24
go.mod
View File

@ -4,17 +4,15 @@ go 1.16
require (
github.com/Masterminds/squirrel v1.5.3
github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go v1.45.5
github.com/beego/beego v1.12.12
github.com/beevik/etree v1.1.0
github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.77.2
github.com/casdoor/go-sms-sender v0.15.0
github.com/casdoor/go-sms-sender v0.19.0
github.com/casdoor/gomail/v2 v2.0.1
github.com/casdoor/notify v0.45.0
github.com/casdoor/oss v1.3.0
github.com/casdoor/oss v1.4.1
github.com/casdoor/xorm-adapter/v3 v3.1.0
github.com/casvisor/casvisor-go-sdk v1.0.3
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
@ -23,6 +21,7 @@ require (
github.com/elimity-com/scim v0.0.0-20230426070224-941a5eac92f3
github.com/fogleman/gg v1.3.0
github.com/forestmgy/ldapserver v1.1.0
github.com/go-asn1-ber/asn1-ber v1.5.5
github.com/go-git/go-git/v5 v5.6.0
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-mysql-org/go-mysql v1.7.0
@ -31,19 +30,17 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/go-webauthn/webauthn v0.6.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.1
github.com/json-iterator/go v1.1.12 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/google/uuid v1.4.0
github.com/json-iterator/go v1.1.12
github.com/lestrrat-go/jwx v1.2.21
github.com/lib/pq v1.10.9
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
github.com/markbates/goth v1.75.2
github.com/mitchellh/mapstructure v1.5.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.11.1
github.com/prometheus/client_model v0.3.0
github.com/prometheus/client_model v0.4.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.9.0
@ -62,11 +59,10 @@ require (
github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.13.0
golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.11.0
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.138.0
golang.org/x/crypto v0.14.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.13.0
google.golang.org/api v0.150.0
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68

295
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "Der Anbieter: %s ist nicht für die Anwendung aktiviert",
"Unauthorized operation": "Nicht autorisierte Operation",
"Unknown authentication type (not password or provider), form = %s": "Unbekannter Authentifizierungstyp (nicht Passwort oder Anbieter), Formular = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s und %s stimmen nicht überein"
@ -33,11 +34,12 @@
"Email is invalid": "E-Mail ist ungültig",
"Empty username.": "Leerer Benutzername.",
"FirstName cannot be blank": "Vorname darf nicht leer sein",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Ldap Benutzername oder Passwort falsch",
"LastName cannot be blank": "Nachname darf nicht leer sein",
"Multiple accounts with same uid, please check your ldap server": "Mehrere Konten mit derselben uid, bitte überprüfen Sie Ihren LDAP-Server",
"Organization does not exist": "Organisation existiert nicht",
"Password must have at least 6 characters": "Das Passwort muss mindestens 6 Zeichen enthalten",
"Phone already exists": "Telefon existiert bereits",
"Phone cannot be empty": "Das Telefon darf nicht leer sein",
"Phone number is invalid": "Die Telefonnummer ist ungültig",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "El proveedor: %s no está habilitado para la aplicación",
"Unauthorized operation": "Operación no autorizada",
"Unknown authentication type (not password or provider), form = %s": "Tipo de autenticación desconocido (no es contraseña o proveedor), formulario = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Los servicios %s y %s no coinciden"
@ -33,11 +34,12 @@
"Email is invalid": "El correo electrónico no es válido",
"Empty username.": "Nombre de usuario vacío.",
"FirstName cannot be blank": "El nombre no puede estar en blanco",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Nombre de usuario o contraseña de Ldap incorrectos",
"LastName cannot be blank": "El apellido no puede estar en blanco",
"Multiple accounts with same uid, please check your ldap server": "Cuentas múltiples con el mismo uid, por favor revise su servidor ldap",
"Organization does not exist": "La organización no existe",
"Password must have at least 6 characters": "La contraseña debe tener al menos 6 caracteres",
"Phone already exists": "El teléfono ya existe",
"Phone cannot be empty": "Teléfono no puede estar vacío",
"Phone number is invalid": "El número de teléfono no es válido",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "Le fournisseur :%s n'est pas activé pour l'application",
"Unauthorized operation": "Opération non autorisée",
"Unknown authentication type (not password or provider), form = %s": "Type d'authentification inconnu (pas de mot de passe ou de fournisseur), formulaire = %s",
"User's tag: %s is not listed in the application's tags": "Le tag de lutilisateur %s nest pas répertorié dans les tags de lapplication"
"User's tag: %s is not listed in the application's tags": "Le tag de lutilisateur %s nest pas répertorié dans les tags de lapplication",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Les services %s et %s ne correspondent pas"
@ -33,11 +34,12 @@
"Email is invalid": "L'adresse e-mail est invalide",
"Empty username.": "Nom d'utilisateur vide.",
"FirstName cannot be blank": "Le prénom ne peut pas être laissé vide",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Nom d'utilisateur ou mot de passe LDAP incorrect",
"LastName cannot be blank": "Le nom de famille ne peut pas être vide",
"Multiple accounts with same uid, please check your ldap server": "Plusieurs comptes avec le même identifiant d'utilisateur, veuillez vérifier votre serveur LDAP",
"Organization does not exist": "L'organisation n'existe pas",
"Password must have at least 6 characters": "Le mot de passe doit comporter au moins 6 caractères",
"Phone already exists": "Le téléphone existe déjà",
"Phone cannot be empty": "Le téléphone ne peut pas être vide",
"Phone number is invalid": "Le numéro de téléphone est invalide",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "Penyedia: %s tidak diaktifkan untuk aplikasi ini",
"Unauthorized operation": "Operasi tidak sah",
"Unknown authentication type (not password or provider), form = %s": "Jenis otentikasi tidak diketahui (bukan kata sandi atau pemberi), formulir = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Layanan %s dan %s tidak cocok"
@ -33,11 +34,12 @@
"Email is invalid": "Email tidak valid",
"Empty username.": "Nama pengguna kosong.",
"FirstName cannot be blank": "Nama depan tidak boleh kosong",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Nama pengguna atau kata sandi Ldap salah",
"LastName cannot be blank": "Nama belakang tidak boleh kosong",
"Multiple accounts with same uid, please check your ldap server": "Beberapa akun dengan uid yang sama, harap periksa server ldap Anda",
"Organization does not exist": "Organisasi tidak ada",
"Password must have at least 6 characters": "Kata sandi harus memiliki minimal 6 karakter",
"Phone already exists": "Telepon sudah ada",
"Phone cannot be empty": "Telepon tidak boleh kosong",
"Phone number is invalid": "Nomor telepon tidak valid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "プロバイダー:%sはアプリケーションでは有効化されていません",
"Unauthorized operation": "不正操作",
"Unknown authentication type (not password or provider), form = %s": "不明な認証タイプ(パスワードまたはプロバイダーではない)フォーム=%s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "サービス%sと%sは一致しません"
@ -33,11 +34,12 @@
"Email is invalid": "電子メールは無効です",
"Empty username.": "空のユーザー名。",
"FirstName cannot be blank": "ファーストネームは空白にできません",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Ldapのユーザー名またはパスワードが間違っています",
"LastName cannot be blank": "姓は空白にできません",
"Multiple accounts with same uid, please check your ldap server": "同じuidを持つ複数のアカウントがあります。あなたのLDAPサーバーを確認してください",
"Organization does not exist": "組織は存在しません",
"Password must have at least 6 characters": "パスワードは少なくとも6つの文字が必要です",
"Phone already exists": "電話はすでに存在しています",
"Phone cannot be empty": "電話は空っぽにできません",
"Phone number is invalid": "電話番号が無効です",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "제공자 %s은(는) 응용 프로그램에서 활성화되어 있지 않습니다",
"Unauthorized operation": "무단 조작",
"Unknown authentication type (not password or provider), form = %s": "알 수 없는 인증 유형(암호 또는 공급자가 아님), 폼 = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "서비스 %s와 %s는 일치하지 않습니다"
@ -33,11 +34,12 @@
"Email is invalid": "이메일이 유효하지 않습니다",
"Empty username.": "빈 사용자 이름.",
"FirstName cannot be blank": "이름은 공백일 수 없습니다",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP 사용자 이름 또는 암호가 잘못되었습니다",
"LastName cannot be blank": "성은 비어 있을 수 없습니다",
"Multiple accounts with same uid, please check your ldap server": "동일한 UID를 가진 여러 계정이 있습니다. LDAP 서버를 확인해주세요",
"Organization does not exist": "조직은 존재하지 않습니다",
"Password must have at least 6 characters": "암호는 적어도 6자 이상이어야 합니다",
"Phone already exists": "전화기는 이미 존재합니다",
"Phone cannot be empty": "전화는 비워 둘 수 없습니다",
"Phone number is invalid": "전화번호가 유효하지 않습니다",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "Провайдер: %s не включен для приложения",
"Unauthorized operation": "Несанкционированная операция",
"Unknown authentication type (not password or provider), form = %s": "Неизвестный тип аутентификации (не пароль и не провайдер), форма = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Сервисы %s и %s не совпадают"
@ -33,11 +34,12 @@
"Email is invalid": "Адрес электронной почты недействительный",
"Empty username.": "Пустое имя пользователя.",
"FirstName cannot be blank": "Имя не может быть пустым",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Неправильное имя пользователя или пароль Ldap",
"LastName cannot be blank": "Фамилия не может быть пустой",
"Multiple accounts with same uid, please check your ldap server": "Множественные учетные записи с тем же UID. Пожалуйста, проверьте свой сервер LDAP",
"Organization does not exist": "Организация не существует",
"Password must have at least 6 characters": "Пароль должен содержать не менее 6 символов",
"Phone already exists": "Телефон уже существует",
"Phone cannot be empty": "Телефон не может быть пустым",
"Phone number is invalid": "Номер телефона является недействительным",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
@ -33,11 +34,12 @@
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "Nhà cung cấp: %s không được kích hoạt cho ứng dụng",
"Unauthorized operation": "Hoạt động không được ủy quyền",
"Unknown authentication type (not password or provider), form = %s": "Loại xác thực không xác định (không phải mật khẩu hoặc nhà cung cấp), biểu mẫu = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "Dịch sang tiếng Việt: Dịch vụ %s và %s không khớp"
@ -33,11 +34,12 @@
"Email is invalid": "Địa chỉ email không hợp lệ",
"Empty username.": "Tên đăng nhập trống.",
"FirstName cannot be blank": "Tên không được để trống",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "Tên người dùng hoặc mật khẩu Ldap không chính xác",
"LastName cannot be blank": "Họ không thể để trống",
"Multiple accounts with same uid, please check your ldap server": "Nhiều tài khoản với cùng một uid, vui lòng kiểm tra máy chủ ldap của bạn",
"Organization does not exist": "Tổ chức không tồn tại",
"Password must have at least 6 characters": "Mật khẩu phải ít nhất 6 ký tự",
"Phone already exists": "Điện thoại đã tồn tại",
"Phone cannot be empty": "Điện thoại không thể để trống",
"Phone number is invalid": "Số điện thoại không hợp lệ",

View File

@ -19,7 +19,8 @@
"The provider: %s is not enabled for the application": "该应用的提供商: %s未被启用",
"Unauthorized operation": "未授权的操作",
"Unknown authentication type (not password or provider), form = %s": "未知的认证类型(非密码或第三方提供商):%s",
"User's tag: %s is not listed in the application's tags": "用户的标签: %s不在该应用的标签列表中"
"User's tag: %s is not listed in the application's tags": "用户的标签: %s不在该应用的标签列表中",
"paid-user %s does not have active or pending subscription and the application: %s does not have default pricing": "paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"
},
"cas": {
"Service %s and %s do not match": "服务%s与%s不匹配"
@ -33,11 +34,12 @@
"Email is invalid": "无效邮箱",
"Empty username.": "用户名不可为空",
"FirstName cannot be blank": "名不可以为空",
"Invitation code cannot be blank": "Invitation code cannot be blank",
"Invitation code is invalid": "Invitation code is invalid",
"LDAP user name or password incorrect": "LDAP密码错误",
"LastName cannot be blank": "姓不可以为空",
"Multiple accounts with same uid, please check your ldap server": "多个帐户具有相同的uid请检查您的 LDAP 服务器",
"Organization does not exist": "组织不存在",
"Password must have at least 6 characters": "新密码至少为6位",
"Phone already exists": "该手机号已存在",
"Phone cannot be empty": "手机号不可为空",
"Phone number is invalid": "无效手机号",

View File

@ -85,10 +85,12 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
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 := io.ReadAll(resp.Body)
if err != nil {
return nil, err
@ -97,10 +99,10 @@ func (idp *AdfsIdProvider) GetToken(code string) (*oauth2.Token, error) {
pToken := &AdfsToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, fmt.Errorf("fail to unmarshal token response: %s", err.Error())
return nil, err
}
if pToken.ErrMsg != "" {
return nil, fmt.Errorf("pToken.Errmsg = %s", pToken.ErrMsg)
return nil, fmt.Errorf(pToken.ErrMsg)
}
token := &oauth2.Token{

126
idp/azuread_b2c.go Normal file
View File

@ -0,0 +1,126 @@
// Copyright 2023 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"
"net/http"
"net/url"
"time"
"golang.org/x/oauth2"
)
type AzureADB2CProvider struct {
Client *http.Client
Config *oauth2.Config
Tenant string
UserFlow string
}
func NewAzureAdB2cProvider(clientId, clientSecret, redirectUrl, tenant string, userFlow string) *AzureADB2CProvider {
return &AzureADB2CProvider{
Config: &oauth2.Config{
ClientID: clientId,
ClientSecret: clientSecret,
RedirectURL: redirectUrl,
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s.b2clogin.com/%s.onmicrosoft.com/%s/oauth2/v2.0/authorize", tenant, tenant, userFlow),
TokenURL: fmt.Sprintf("https://%s.b2clogin.com/%s.onmicrosoft.com/%s/oauth2/v2.0/token", tenant, tenant, userFlow),
},
Scopes: []string{"openid", "email"},
},
Tenant: tenant,
UserFlow: userFlow,
}
}
func (p *AzureADB2CProvider) SetHttpClient(client *http.Client) {
p.Client = client
}
type AzureadB2cToken struct {
IdToken string `json:"id_token"`
TokenType string `json:"token_type"`
NotBefore int `json:"not_before"`
IdTokenExpiresIn int `json:"id_token_expires_in"`
ProfileInfo string `json:"profile_info"`
Scope string `json:"scope"`
}
func (p *AzureADB2CProvider) GetToken(code string) (*oauth2.Token, error) {
payload := url.Values{}
payload.Set("code", code)
payload.Set("grant_type", "authorization_code")
payload.Set("client_id", p.Config.ClientID)
payload.Set("client_secret", p.Config.ClientSecret)
payload.Set("redirect_uri", p.Config.RedirectURL)
resp, err := p.Client.PostForm(p.Config.Endpoint.TokenURL, payload)
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
pToken := &AzureadB2cToken{}
err = json.Unmarshal(data, pToken)
if err != nil {
return nil, err
}
token := &oauth2.Token{
AccessToken: pToken.IdToken,
Expiry: time.Unix(time.Now().Unix()+int64(pToken.IdTokenExpiresIn), 0),
}
return token, nil
}
func (p *AzureADB2CProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
userInfoEndpoint := fmt.Sprintf("https://%s.b2clogin.com/%s.onmicrosoft.com/%s/openid/v2.0/userinfo", p.Tenant, p.Tenant, p.UserFlow)
req, err := http.NewRequest("GET", userInfoEndpoint, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
resp, err := p.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching user info: status code %d", resp.StatusCode)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var userInfo UserInfo
err = json.Unmarshal(bodyBytes, &userInfo)
if err != nil {
return nil, err
}
return &userInfo, nil
}

View File

@ -91,6 +91,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error
return NewGitlabIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "ADFS":
return NewAdfsIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl), nil
case "AzureADB2C":
return NewAzureAdB2cProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl, idpInfo.HostUrl, idpInfo.AppId), nil
case "Baidu":
return NewBaiduIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil
case "Alipay":

View File

@ -104,7 +104,9 @@
}
],
"redirectUris": [""],
"expireInHours": 168
"expireInHours": 168,
"failedSigninLimit": 5,
"failedSigninfrozenTime": 15
}
],
"users": [

View File

@ -1,5 +1,5 @@
apiVersion: v2
name: casdoor
name: casdoor-helm-charts
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
version: 0.3.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"
appVersion: "1.18.0"

View File

@ -59,6 +59,9 @@ spec:
volumeMounts:
- name: config-volume
mountPath: /conf
{{ if .Values.extraContainersEnabled }}
{{- .Values.extraContainers | nindent 8 }}
{{- end }}
volumes:
- name: config-volume
projected:

View File

@ -108,3 +108,10 @@ nodeSelector: {}
tolerations: []
affinity: {}
# -- Optionally add extra sidecar containers.
extraContainersEnabled: false
extraContainers: ""
# extraContainers: |
# - name: ...
# image: ...

View File

@ -15,10 +15,11 @@
package notification
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/casdoor/casdoor/proxy"
)
@ -39,26 +40,29 @@ func NewCustomHttpProvider(endpoint string, method string, paramName string) (*H
}
func (c *HttpNotificationClient) Send(ctx context.Context, subject string, content string) error {
var req *http.Request
var err error
httpClient := proxy.DefaultHttpClient
req, err := http.NewRequest(c.method, c.endpoint, bytes.NewBufferString(content))
if err != nil {
return err
}
if c.method == "POST" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.PostForm = map[string][]string{
c.paramName: {content},
formValues := url.Values{}
formValues.Set(c.paramName, content)
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else if c.method == "GET" {
req, err = http.NewRequest(c.method, c.endpoint, nil)
if err != nil {
return err
}
q := req.URL.Query()
q.Add(c.paramName, content)
req.URL.RawQuery = q.Encode()
}
httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req)
if err != nil {
return err
@ -66,7 +70,7 @@ func (c *HttpNotificationClient) Send(ctx context.Context, subject string, conte
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("SendMessage() error, custom HTTP Notification request failed with status: %s", resp.Status)
return fmt.Errorf("HttpNotificationClient's SendMessage() error, custom HTTP Notification request failed with status: %s", resp.Status)
}
return err

View File

@ -90,6 +90,9 @@ type Application struct {
FormOffset int `json:"formOffset"`
FormSideHtml string `xorm:"mediumtext" json:"formSideHtml"`
FormBackgroundUrl string `xorm:"varchar(200)" json:"formBackgroundUrl"`
FailedSigninLimit int `json:"failedSigninLimit"`
FailedSigninfrozenTime int `json:"failedSigninfrozenTime"`
}
func GetApplicationCount(owner, field, value string) (int64, error) {

View File

@ -28,8 +28,9 @@ import (
)
const (
SigninWrongTimesLimit = 5
LastSignWrongTimeDuration = time.Minute * 15
DefaultFailedSigninLimit = 5
// DefaultFailedSigninfrozenTime The unit of frozen time is minutes
DefaultFailedSigninfrozenTime = 15
)
func CheckUserSignup(application *Application, organization *Organization, form *form.AuthForm, lang string) string {
@ -143,10 +144,15 @@ func CheckUserSignup(application *Application, organization *Organization, form
}
func checkSigninErrorTimes(user *User, lang string) error {
if user.SigninWrongTimes >= SigninWrongTimesLimit {
failedSigninLimit, failedSigninfrozenTime, err := GetFailedSigninConfigByUser(user)
if err != nil {
return err
}
if user.SigninWrongTimes >= failedSigninLimit {
lastSignWrongTime, _ := time.Parse(time.RFC3339, user.LastSigninWrongTime)
passedTime := time.Now().UTC().Sub(lastSignWrongTime)
minutes := int(LastSignWrongTimeDuration.Minutes() - passedTime.Minutes())
minutes := failedSigninfrozenTime - int(passedTime.Minutes())
// deny the login if the error times is greater than the limit and the last login time is less than the duration
if minutes > 0 {
@ -237,22 +243,28 @@ func checkLdapUserPassword(user *User, password string, lang string) error {
searchResult, err := conn.Conn.Search(searchReq)
if err != nil {
conn.Close()
return err
}
if len(searchResult.Entries) == 0 {
conn.Close()
continue
}
if len(searchResult.Entries) > 1 {
conn.Close()
return fmt.Errorf(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
}
hit = true
dn := searchResult.Entries[0].DN
if err := conn.Conn.Bind(dn, password); err == nil {
if err = conn.Conn.Bind(dn, password); err == nil {
ldapLoginSuccess = true
conn.Close()
break
}
conn.Close()
}
if !ldapLoginSuccess {
@ -473,7 +485,14 @@ func CheckToEnableCaptcha(application *Application, organization, username strin
if err != nil {
return false, err
}
return user != nil && user.SigninWrongTimes >= SigninWrongTimesLimit, nil
var failedSigninLimit int
if application.FailedSigninLimit == 0 {
failedSigninLimit = 5
} else {
failedSigninLimit = application.FailedSigninLimit
}
return user != nil && user.SigninWrongTimes >= failedSigninLimit, nil
}
return providerItem.Rule == "Always", nil
}

View File

@ -47,18 +47,42 @@ func resetUserSigninErrorTimes(user *User) error {
return err
}
func GetFailedSigninConfigByUser(user *User) (int, int, error) {
application, err := GetApplicationByUser(user)
if err != nil {
return 0, 0, err
}
failedSigninLimit := application.FailedSigninLimit
failedSigninfrozenTime := application.FailedSigninfrozenTime
// 0 as an initialization value, corresponding to the default configuration parameters
if failedSigninLimit == 0 {
failedSigninLimit = DefaultFailedSigninLimit
}
if failedSigninfrozenTime == 0 {
failedSigninfrozenTime = DefaultFailedSigninfrozenTime
}
return failedSigninLimit, failedSigninfrozenTime, nil
}
func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
failedSigninLimit, failedSigninfrozenTime, errSignin := GetFailedSigninConfigByUser(user)
if errSignin != nil {
return errSignin
}
// increase failed login count
if user.SigninWrongTimes < SigninWrongTimesLimit {
if user.SigninWrongTimes < failedSigninLimit {
user.SigninWrongTimes++
}
if user.SigninWrongTimes >= SigninWrongTimesLimit {
if user.SigninWrongTimes >= failedSigninLimit {
// record the latest failed login time
user.LastSigninWrongTime = time.Now().UTC().Format(time.RFC3339)
}
@ -69,7 +93,7 @@ func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
return err
}
leftChances := SigninWrongTimesLimit - user.SigninWrongTimes
leftChances := failedSigninLimit - user.SigninWrongTimes
if leftChances == 0 && enableCaptcha {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
@ -77,5 +101,5 @@ func recordSigninErrorInfo(user *User, lang string, options ...bool) error {
}
// don't show the chance error message if the user has no chance left
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), failedSigninfrozenTime)
}

View File

@ -36,7 +36,7 @@ func getDialer(provider *Provider) *gomail.Dialer {
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl)
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method)
fromAddress := provider.ClientId2
if fromAddress == "" {

View File

@ -125,6 +125,10 @@ func (enforcer *Enforcer) GetId() string {
return fmt.Sprintf("%s/%s", enforcer.Owner, enforcer.Name)
}
func (enforcer *Enforcer) GetModelAndAdapter() string {
return util.GetId(enforcer.Model, enforcer.Adapter)
}
func (enforcer *Enforcer) InitEnforcer() error {
if enforcer.Enforcer != nil {
return nil

View File

@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/builder"
"github.com/xorm-io/core"
@ -224,7 +225,8 @@ func GetGroupUserCount(groupId string, field, value string) (int64, error) {
if field == "" && value == "" {
return int64(len(names)), nil
} else {
return ormer.Engine.Table("user").
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
return ormer.Engine.Table(tableNamePrefix+"user").
Where("owner = ?", owner).In("name", names).
And(fmt.Sprintf("user.%s like ?", util.CamelToSnakeCase(field)), "%"+value+"%").
Count()
@ -239,7 +241,8 @@ func GetPaginationGroupUsers(groupId string, offset, limit int, field, value, so
return nil, err
}
session := ormer.Engine.Table("user").
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
session := ormer.Engine.Table(tableNamePrefix+"user").
Where("owner = ?", owner).In("name", names)
if offset != -1 && limit != -1 {
@ -271,7 +274,9 @@ func GetGroupUsers(groupId string) ([]*User, error) {
users := []*User{}
owner, _ := util.GetOwnerAndNameFromId(groupId)
names, err := userEnforcer.GetUserNamesByGroupName(groupId)
if err != nil {
return nil, err
}
err = ormer.Engine.Where("owner = ?", owner).In("name", names).Find(&users)
if err != nil {
return nil, err
@ -303,6 +308,9 @@ func GroupChangeTrigger(oldName, newName string) error {
groups := []*Group{}
err = session.Where("parent_id = ?", oldName).Find(&groups)
if err != nil {
return err
}
for _, group := range groups {
group.ParentId = newName
_, err := session.ID(core.PK{group.Owner, group.Name}).Cols("parent_id").Update(group)

134
object/invitation.go Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2023 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"
"github.com/xorm-io/core"
)
type Invitation 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"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Code string `xorm:"varchar(100)" json:"code"`
Quota int `json:"quota"`
UsedCount int `json:"usedCount"`
Application string `xorm:"varchar(100)" json:"application"`
Username string `xorm:"varchar(100)" json:"username"`
Email string `xorm:"varchar(100)" json:"email"`
Phone string `xorm:"varchar(100)" json:"phone"`
SignupGroup string `xorm:"varchar(100)" json:"signupGroup"`
State string `xorm:"varchar(100)" json:"state"`
}
func GetInvitationCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Invitation{})
}
func GetInvitations(owner string) ([]*Invitation, error) {
invitations := []*Invitation{}
err := ormer.Engine.Desc("created_time").Find(&invitations, &Invitation{Owner: owner})
if err != nil {
return invitations, err
}
return invitations, nil
}
func GetPaginationInvitations(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Invitation, error) {
invitations := []*Invitation{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&invitations)
if err != nil {
return invitations, err
}
return invitations, nil
}
func getInvitation(owner string, name string) (*Invitation, error) {
if owner == "" || name == "" {
return nil, nil
}
invitation := Invitation{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&invitation)
if err != nil {
return &invitation, nil
}
if existed {
return &invitation, nil
} else {
return nil, nil
}
}
func GetInvitation(id string) (*Invitation, error) {
owner, name := util.GetOwnerAndNameFromId(id)
return getInvitation(owner, name)
}
func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id)
if p, err := getInvitation(owner, name); err != nil {
return false, err
} else if p == nil {
return false, nil
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation)
if err != nil {
return false, err
}
return affected != 0, nil
}
func AddInvitation(invitation *Invitation) (bool, error) {
affected, err := ormer.Engine.Insert(invitation)
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteInvitation(invitation *Invitation) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{invitation.Owner, invitation.Name}).Delete(&Invitation{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func (invitation *Invitation) GetId() string {
return fmt.Sprintf("%s/%s", invitation.Owner, invitation.Name)
}
func VerifyInvitation(id string) (payment *Payment, attachInfo map[string]interface{}, err error) {
return nil, nil, fmt.Errorf("the invitation: %s does not exist", id)
}

View File

@ -100,6 +100,7 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
users, err := conn.GetLdapUsers(ldap)
if err != nil {
conn.Close()
logs.Warning(fmt.Sprintf("autoSync failed for %s, error %s", ldap.Id, err))
continue
}
@ -111,6 +112,8 @@ func (l *LdapAutoSynchronizer) syncRoutine(ldap *Ldap, stopChan chan struct{}) e
} else {
logs.Info(fmt.Sprintf("ldap autosync success, %d new users, %d existing users", len(users)-len(existed), len(existed)))
}
conn.Close()
}
}

View File

@ -19,6 +19,7 @@ import (
"fmt"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
goldap "github.com/go-ldap/ldap/v3"
"github.com/thanhpk/randstr"
@ -81,6 +82,17 @@ func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
return &LdapConn{Conn: conn, IsAD: isAD}, nil
}
func (l *LdapConn) Close() {
if l.Conn == nil {
return
}
err := l.Conn.Unbind()
if err != nil {
panic(err)
}
}
func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
SearchFilter := "(objectClass=*)"
SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}
@ -345,7 +357,8 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
func GetExistUuids(owner string, uuids []string) ([]string, error) {
var existUuids []string
err := ormer.Engine.Table("user").Where("owner = ?", owner).Cols("ldap").
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
In("ldap", uuids).Select("DISTINCT ldap").Find(&existUuids)
if err != nil {
return existUuids, err

View File

@ -234,12 +234,37 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Group))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(User))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Group))
err = a.Engine.Sync2(new(Invitation))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Application))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Provider))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Resource))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Cert))
if err != nil {
panic(err)
}
@ -269,17 +294,7 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Provider))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Application))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Resource))
err = a.Engine.Sync2(new(Session))
if err != nil {
panic(err)
}
@ -289,26 +304,6 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(VerificationRecord))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Webhook))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Syncer))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Cert))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Product))
if err != nil {
panic(err)
@ -319,6 +314,36 @@ func (a *Ormer) createTable() {
panic(err)
}
err = a.Engine.Sync2(new(Plan))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Pricing))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Subscription))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Syncer))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Webhook))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(VerificationRecord))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Ldap))
if err != nil {
panic(err)
@ -333,24 +358,4 @@ func (a *Ormer) createTable() {
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Session))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Subscription))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Plan))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Pricing))
if err != nil {
panic(err)
}
}

View File

@ -120,7 +120,11 @@ func checkPermissionValid(permission *Permission) error {
return nil
}
groupingPolicies := getGroupingPolicies(permission)
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
@ -442,9 +446,8 @@ func GetMaskedPermissions(permissions []*Permission) []*Permission {
// as the policyFilter when the enforcer load policy).
func GroupPermissionsByModelAdapter(permissions []*Permission) map[string][]string {
m := make(map[string][]string)
for _, permission := range permissions {
key := permission.Model + permission.Adapter
key := permission.GetModelAndAdapter()
permissionIds, ok := m[key]
if !ok {
m[key] = []string{permission.GetId()}
@ -460,9 +463,17 @@ func (p *Permission) GetId() string {
return util.GetId(p.Owner, p.Name)
}
func (p *Permission) GetModelAndAdapter() string {
return util.GetId(p.Model, p.Adapter)
}
func (p *Permission) isUserHit(name string) bool {
targetOrg, targetName := util.GetOwnerAndNameFromId(name)
for _, user := range p.Users {
if user == "*" {
return true
}
userOrg, userName := util.GetOwnerAndNameFromId(user)
if userOrg == targetOrg && (userName == "*" || userName == targetName) {
return true
@ -476,9 +487,14 @@ func (p *Permission) isRoleHit(userId string) bool {
if err != nil {
return false
}
for _, role := range p.Roles {
if role == "*" {
return true
}
for _, targetRole := range targetRoles {
if targetRole.GetId() == role {
if role == targetRole.GetId() {
return true
}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/casbin/casbin/v2/log"
"github.com/casbin/casbin/v2/model"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
xormadapter "github.com/casdoor/xorm-adapter/v3"
)
@ -137,6 +138,16 @@ func getPolicies(permission *Permission) [][]string {
}
func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error) {
roleOwner, roleName := util.GetOwnerAndNameFromId(roleId)
if roleName == "*" {
roles, err := GetRoles(roleOwner)
if err != nil {
return []*Role{}, err
}
return roles, nil
}
role, err := GetRole(roleId)
if err != nil {
return []*Role{}, err
@ -162,7 +173,7 @@ func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error)
return roles, nil
}
func getGroupingPolicies(permission *Permission) [][]string {
func getGroupingPolicies(permission *Permission) ([][]string, error) {
var groupingPolicies [][]string
domainExist := len(permission.Domains) > 0
@ -170,12 +181,18 @@ func getGroupingPolicies(permission *Permission) [][]string {
for _, roleId := range permission.Roles {
visited := map[string]struct{}{}
if roleId == "*" {
roleId = util.GetId(permission.Owner, "*")
}
rolesInRole, err := getRolesInRole(roleId, visited)
if err != nil {
panic(err)
return nil, err
}
for _, role := range rolesInRole {
roleId := role.GetId()
roleId = role.GetId()
for _, subUser := range role.Users {
if domainExist {
for _, domain := range permission.Domains {
@ -198,7 +215,7 @@ func getGroupingPolicies(permission *Permission) [][]string {
}
}
return groupingPolicies
return groupingPolicies, nil
}
func addPolicies(permission *Permission) error {
@ -231,7 +248,10 @@ func addGroupingPolicies(permission *Permission) error {
return err
}
groupingPolicies := getGroupingPolicies(permission)
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
@ -249,7 +269,10 @@ func removeGroupingPolicies(permission *Permission) error {
return err
}
groupingPolicies := getGroupingPolicies(permission)
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.RemoveGroupingPolicies(groupingPolicies)
@ -261,24 +284,28 @@ func removeGroupingPolicies(permission *Permission) error {
return nil
}
type CasbinRequest = []interface{}
func Enforce(permission *Permission, request *CasbinRequest, permissionIds ...string) (bool, error) {
func Enforce(permission *Permission, request []string, permissionIds ...string) (bool, error) {
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {
return false, err
}
return enforcer.Enforce(*request...)
// type transformation
interfaceRequest := util.StringToInterfaceArray(request)
return enforcer.Enforce(interfaceRequest...)
}
func BatchEnforce(permission *Permission, requests *[]CasbinRequest, permissionIds ...string) ([]bool, error) {
func BatchEnforce(permission *Permission, requests [][]string, permissionIds ...string) ([]bool, error) {
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {
return nil, err
}
return enforcer.BatchEnforce(*requests)
// type transformation
interfaceRequests := util.StringToInterfaceArray2d(requests)
return enforcer.BatchEnforce(interfaceRequests)
}
func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) ([]string, error) {
@ -287,7 +314,12 @@ func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) ([
return nil, err
}
for _, role := range GetAllRoles(userId) {
allRoles, err := GetAllRoles(userId)
if err != nil {
return nil, err
}
for _, role := range allRoles {
permissionsByRole, err := GetPermissionsByRole(role)
if err != nil {
return nil, err
@ -321,17 +353,17 @@ func GetAllActions(userId string) ([]string, error) {
})
}
func GetAllRoles(userId string) []string {
func GetAllRoles(userId string) ([]string, error) {
roles, err := getRolesByUser(userId)
if err != nil {
panic(err)
return nil, err
}
var res []string
res := []string{}
for _, role := range roles {
res = append(res, role.Name)
}
return res
return res, nil
}
func GetBuiltInModel(modelText string) (model.Model, error) {

View File

@ -37,7 +37,7 @@ type Provider struct {
SubType string `xorm:"varchar(100)" json:"subType"`
Method string `xorm:"varchar(100)" json:"method"`
ClientId string `xorm:"varchar(200)" json:"clientId"`
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
ClientSecret string `xorm:"varchar(3000)" json:"clientSecret"`
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
ClientSecret2 string `xorm:"varchar(500)" json:"clientSecret2"`
Cert string `xorm:"varchar(100)" json:"cert"`
@ -52,7 +52,7 @@ type Provider struct {
Port int `json:"port"`
DisableSsl bool `json:"disableSsl"` // If the provider type is WeChat, DisableSsl means EnableQRCode
Title string `xorm:"varchar(100)" json:"title"`
Content string `xorm:"varchar(1000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
Content string `xorm:"varchar(2000)" json:"content"` // If provider type is WeChat, Content means QRCode string by Base64 encoding
Receiver string `xorm:"varchar(100)" json:"receiver"`
RegionId string `xorm:"varchar(100)" json:"regionId"`
@ -417,7 +417,7 @@ func FromProviderToIdpInfo(ctx *context.Context, provider *Provider) *idp.Provid
providerInfo.ClientId = provider.ClientId2
providerInfo.ClientSecret = provider.ClientSecret2
}
} else if provider.Type == "AzureAD" || provider.Type == "ADFS" || provider.Type == "Okta" {
} else if provider.Type == "AzureAD" || provider.Type == "AzureADB2C" || provider.Type == "ADFS" || provider.Type == "Okta" {
providerInfo.HostUrl = provider.Domain
}

View File

@ -9,7 +9,6 @@ import (
)
// https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/sec_usr_radatt/configuration/xe-16/sec-usr-radatt-xe-16-book/sec-rad-ov-ietf-attr.html
// https://support.huawei.com/enterprise/zh/doc/EDOC1000178159/35071f9a
type RadiusAccounting struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`

View File

@ -271,6 +271,9 @@ func getRolesByUserInternal(userId string) ([]*Role, error) {
if err != nil {
return roles, err
}
if user == nil {
return nil, fmt.Errorf("The user: %s doesn't exist", userId)
}
query := ormer.Engine.Alias("r").Where("r.users like ?", fmt.Sprintf("%%%s%%", userId))
for _, group := range user.Groups {

View File

@ -28,10 +28,10 @@ import (
"io"
"time"
"github.com/RobotsAndPencils/go-saml"
"github.com/beevik/etree"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
saml "github.com/russellhaering/gosaml2"
dsig "github.com/russellhaering/goxmldsig"
)
@ -283,15 +283,15 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
}
}
var authnRequest saml.AuthnRequest
var authnRequest saml.AuthNRequest
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
if err != nil {
return "", "", method, fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request. %s", err.Error())
}
// verify samlRequest
if isValid := application.IsRedirectUriValid(authnRequest.Issuer.Url); !isValid {
return "", "", method, fmt.Errorf("err: Issuer URI: %s doesn't exist in the allowed Redirect URI list", authnRequest.Issuer.Url)
if isValid := application.IsRedirectUriValid(authnRequest.Issuer); !isValid {
return "", "", method, fmt.Errorf("err: Issuer URI: %s doesn't exist in the allowed Redirect URI list", authnRequest.Issuer)
}
// get certificate string
@ -317,7 +317,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
_, originBackend := getOriginFromHost(host)
// build signedResponse
samlResponse, _ := NewSamlResponse(application, user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer.Url, authnRequest.ID, application.RedirectUris)
samlResponse, _ := NewSamlResponse(application, user, originBackend, certificate, authnRequest.AssertionConsumerServiceURL, authnRequest.Issuer, authnRequest.ID, application.RedirectUris)
randomKeyStore := &X509Key{
PrivateKey: cert.PrivateKey,
X509Certificate: certificate,

View File

@ -26,6 +26,8 @@ func getSmsClient(provider *Provider) (sender.SmsClient, error) {
if provider.Type == sender.HuaweiCloud || provider.Type == sender.AzureACS {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.ProviderUrl, provider.AppId)
} else if provider.Type == "Custom HTTP SMS" {
client, err = newHttpSmsClient(provider.Endpoint, provider.Method, provider.Title)
} else {
client, err = sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.TemplateCode, provider.AppId)
}

83
object/sms_custom_http.go Normal file
View File

@ -0,0 +1,83 @@
// Copyright 2023 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"
"net/http"
"net/url"
"strings"
"github.com/casdoor/casdoor/proxy"
)
type HttpSmsClient struct {
endpoint string
method string
paramName string
}
func newHttpSmsClient(endpoint string, method string, paramName string) (*HttpSmsClient, error) {
client := &HttpSmsClient{
endpoint: endpoint,
method: method,
paramName: paramName,
}
return client, nil
}
func (c *HttpSmsClient) SendMessage(param map[string]string, targetPhoneNumber ...string) error {
phoneNumber := targetPhoneNumber[0]
content := param["code"]
var req *http.Request
var err error
if c.method == "POST" {
formValues := url.Values{}
formValues.Set("phoneNumber", phoneNumber)
formValues.Set(c.paramName, content)
req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else if c.method == "GET" {
req, err = http.NewRequest(c.method, c.endpoint, nil)
if err != nil {
return err
}
q := req.URL.Query()
q.Add("phoneNumber", phoneNumber)
q.Add(c.paramName, content)
req.URL.RawQuery = q.Encode()
} else {
return fmt.Errorf("HttpSmsClient's SendMessage() error, unsupported method: %s", c.method)
}
httpClient := proxy.DefaultHttpClient
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HttpSmsClient's SendMessage() error, custom HTTP SMS request failed with status: %s", resp.Status)
}
return err
}

View File

@ -72,6 +72,10 @@ func GetTruncatedPath(provider *Provider, fullFilePath string, limit int) string
}
func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool) (string, string) {
if provider.Domain != "" && !strings.HasPrefix(provider.Domain, "http://") && !strings.HasPrefix(provider.Domain, "https://") {
provider.Domain = fmt.Sprintf("https://%s", provider.Domain)
}
escapedPath := util.UrlJoin(provider.PathPrefix, fullFilePath)
objectKey := util.UrlJoin(util.GetUrlPath(provider.Domain), escapedPath)
@ -79,9 +83,6 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
if provider.Type != "Local File System" {
// provider.Domain = "https://cdn.casbin.com/casdoor/"
host = util.GetUrlHost(provider.Domain)
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
host = fmt.Sprintf("https://%s", host)
}
} else {
// provider.Domain = "http://localhost:8000" or "https://door.casdoor.com"
host = util.UrlJoin(provider.Domain, "/files")
@ -90,9 +91,12 @@ func GetUploadFileUrl(provider *Provider, fullFilePath string, hasTimestamp bool
host = util.UrlJoin(host, provider.Bucket)
}
fileUrl := util.UrlJoin(host, escapePath(objectKey))
fileUrl := ""
if host != "" {
fileUrl = util.UrlJoin(host, escapePath(objectKey))
}
if hasTimestamp {
if fileUrl != "" && hasTimestamp {
fileUrl = fmt.Sprintf("%s?t=%s", fileUrl, util.GetCurrentUnixTime())
}
@ -112,7 +116,10 @@ func getStorageProvider(provider *Provider, lang string) (oss.StorageInterface,
if provider.Domain == "" {
provider.Domain = storageProvider.GetEndpoint()
UpdateProvider(provider.GetId(), provider)
_, err := UpdateProvider(provider.GetId(), provider)
if err != nil {
return nil, err
}
}
return storageProvider, nil
@ -126,7 +133,12 @@ func uploadFile(provider *Provider, fullFilePath string, fileBuffer *bytes.Buffe
fileUrl, objectKey := GetUploadFileUrl(provider, fullFilePath, true)
_, err = storageProvider.Put(objectKey, fileBuffer)
objectKeyRefined := objectKey
if provider.Type == "Google Cloud Storage" {
objectKeyRefined = strings.TrimPrefix(objectKeyRefined, "/")
}
_, err = storageProvider.Put(objectKeyRefined, fileBuffer)
if err != nil {
return "", "", err
}

View File

@ -144,6 +144,48 @@ func getTokenByCode(code string) (*Token, error) {
return nil, nil
}
func GetTokenByAccessToken(accessToken string) (*Token, error) {
token := Token{AccessTokenHash: getTokenHash(accessToken)}
existed, err := ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
if !existed {
token = Token{AccessToken: accessToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed {
return nil, nil
}
return &token, nil
}
func GetTokenByRefreshToken(refreshToken string) (*Token, error) {
token := Token{RefreshTokenHash: getTokenHash(refreshToken)}
existed, err := ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
if !existed {
token = Token{RefreshToken: refreshToken}
existed, err = ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
}
if !existed {
return nil, nil
}
return &token, nil
}
func updateUsedByCode(token *Token) bool {
affected, err := ormer.Engine.Where("code=?", token.Code).Cols("code_is_used").Update(token)
if err != nil {
@ -219,18 +261,16 @@ func DeleteToken(token *Token) (bool, error) {
}
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
token := Token{AccessTokenHash: getTokenHash(accessToken)}
existed, err := ormer.Engine.Get(&token)
token, err := GetTokenByAccessToken(accessToken)
if err != nil {
return false, nil, nil, err
}
if !existed {
if token == nil {
return false, nil, nil, nil
}
token.ExpiresIn = 0
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(&token)
affected, err := ormer.Engine.ID(core.PK{token.Owner, token.Name}).Cols("expires_in").Update(token)
if err != nil {
return false, nil, nil, err
}
@ -240,22 +280,7 @@ func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, e
return false, nil, nil, err
}
return affected != 0, application, &token, nil
}
func GetTokenByAccessToken(accessToken string) (*Token, error) {
// Check if the accessToken is in the database
token := Token{AccessTokenHash: getTokenHash(accessToken)}
existed, err := ormer.Engine.Get(&token)
if err != nil {
return nil, err
}
if !existed {
return nil, nil
}
return &token, nil
return affected != 0, application, token, nil
}
func GetTokenByTokenAndApplication(token string, application string) (*Token, error) {
@ -457,16 +482,17 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ErrorDescription: "client_id is invalid",
}, nil
}
if clientSecret != "" && application.ClientSecret != clientSecret {
return &TokenError{
Error: InvalidClient,
ErrorDescription: "client_secret is invalid",
}, nil
}
// check whether the refresh token is valid, and has not expired.
token := Token{RefreshTokenHash: getTokenHash(refreshToken)}
existed, err := ormer.Engine.Get(&token)
if err != nil || !existed {
token, err := GetTokenByRefreshToken(refreshToken)
if err != nil || token == nil {
return &TokenError{
Error: InvalidGrant,
ErrorDescription: "refresh token is invalid, expired or revoked",
@ -477,6 +503,12 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
if err != nil {
return nil, err
}
if cert == nil {
return &TokenError{
Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("cert: %s cannot be found", application.Cert),
}, nil
}
_, err = ParseJwtToken(refreshToken, cert)
if err != nil {
@ -485,6 +517,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ErrorDescription: fmt.Sprintf("parse refresh token error: %s", err.Error()),
}, nil
}
// generate a new token
user, err := getUser(application.Organization, token.User)
if err != nil {
@ -502,6 +535,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
if err != nil {
return nil, err
}
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", scope, host)
if err != nil {
return &TokenError{
@ -529,7 +563,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
return nil, err
}
_, err = DeleteToken(&token)
_, err = DeleteToken(token)
if err != nil {
return nil, err
}
@ -542,7 +576,6 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
ExpiresIn: newToken.ExpiresIn,
Scope: newToken.Scope,
}
return tokenWrapper, nil
}

View File

@ -117,6 +117,7 @@ type User struct {
Infoflow string `xorm:"infoflow varchar(100)" json:"infoflow"`
Apple string `xorm:"apple varchar(100)" json:"apple"`
AzureAD string `xorm:"azuread varchar(100)" json:"azuread"`
AzureADB2c string `xorm:"azureadb2c varchar(100)" json:"azureadb2c"`
Slack string `xorm:"slack varchar(100)" json:"slack"`
Steam string `xorm:"steam varchar(100)" json:"steam"`
Bilibili string `xorm:"bilibili varchar(100)" json:"bilibili"`
@ -622,7 +623,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
"eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup",
"microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud",
@ -885,6 +886,18 @@ func (user *User) GetId() string {
return fmt.Sprintf("%s/%s", user.Owner, user.Name)
}
func (user *User) GetFriendlyName() string {
if user.FirstName != "" && user.LastName != "" {
return fmt.Sprintf("%s, %s", user.FirstName, user.LastName)
} else if user.DisplayName != "" {
return user.DisplayName
} else if user.Name != "" {
return user.Name
} else {
return user.Id
}
}
func isUserIdGlobalAdmin(userId string) bool {
return strings.HasPrefix(userId, "built-in/") || strings.HasPrefix(userId, "app/")
}
@ -1021,7 +1034,10 @@ func GenerateIdForNewUser(application *Application) (string, error) {
lastUserId := -1
if lastUser != nil {
lastUserId = util.ParseInt(lastUser.Id)
lastUserId, err = util.ParseIntWithError(lastUser.Id)
if err != nil {
return util.GenerateId(), nil
}
}
res := strconv.Itoa(lastUserId + 1)

View File

@ -89,7 +89,10 @@ func SendVerificationCodeToEmail(organization *Organization, user *User, provide
}
// "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes."
content := fmt.Sprintf(provider.Content, code)
content := strings.Replace(provider.Content, "%s", code, 1)
if user != nil {
content = strings.Replace(content, "%{user.friendlyName}", user.GetFriendlyName(), 1)
}
if err := IsAllowSend(user, remoteAddr, provider.Category); err != nil {
return err

View File

@ -132,6 +132,9 @@ func (pp *StripePaymentProvider) Notify(body []byte, orderId string) (*NotifyRes
}
// Once payment is successful, the Checkout Session will contain a reference to the successful `PaymentIntent`
sIntent, err := stripeIntent.Get(sCheckout.PaymentIntent.ID, nil)
if err != nil {
return nil, err
}
var (
productName string
productDisplayName string

View File

@ -17,15 +17,16 @@ package radius
import (
"fmt"
"log"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"layeh.com/radius"
"layeh.com/radius/rfc2865"
"layeh.com/radius/rfc2866"
)
// https://support.huawei.com/enterprise/zh/doc/EDOC1000178159/35071f9a#tab_3
func StartRadiusServer() {
secret := conf.GetConfigString("radiusSecret")
server := radius.PacketServer{
@ -74,6 +75,11 @@ func handleAccountingRequest(w radius.ResponseWriter, r *radius.Request) {
statusType := rfc2866.AcctStatusType_Get(r.Packet)
username := rfc2865.UserName_GetString(r.Packet)
organization := rfc2865.Class_GetString(r.Packet)
if strings.Contains(username, "/") {
organization, username = util.GetOwnerAndNameFromId(username)
}
log.Printf("handleAccountingRequest() username=%v, org=%v, statusType=%v", username, organization, statusType)
w.Write(r.Response(radius.CodeAccountingResponse))
var err error

View File

@ -73,6 +73,12 @@ func initAPI() {
beego.Router("/api/get-default-application", &controllers.ApiController{}, "GET:GetDefaultApplication")
beego.Router("/api/get-organization-names", &controllers.ApiController{}, "GET:GetOrganizationNames")
beego.Router("/api/get-groups", &controllers.ApiController{}, "GET:GetGroups")
beego.Router("/api/get-group", &controllers.ApiController{}, "GET:GetGroup")
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
beego.Router("/api/get-global-users", &controllers.ApiController{}, "GET:GetGlobalUsers")
beego.Router("/api/get-users", &controllers.ApiController{}, "GET:GetUsers")
beego.Router("/api/get-sorted-users", &controllers.ApiController{}, "GET:GetSortedUsers")
@ -85,11 +91,41 @@ func initAPI() {
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
beego.Router("/api/remove-user-from-group", &controllers.ApiController{}, "POST:RemoveUserFromGroup")
beego.Router("/api/get-groups", &controllers.ApiController{}, "GET:GetGroups")
beego.Router("/api/get-group", &controllers.ApiController{}, "GET:GetGroup")
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation")
beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation")
beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation")
beego.Router("/api/verify-invitation", &controllers.ApiController{}, "GET:VerifyInvitation")
beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications")
beego.Router("/api/get-application", &controllers.ApiController{}, "GET:GetApplication")
beego.Router("/api/get-user-application", &controllers.ApiController{}, "GET:GetUserApplication")
beego.Router("/api/get-organization-applications", &controllers.ApiController{}, "GET:GetOrganizationApplications")
beego.Router("/api/update-application", &controllers.ApiController{}, "POST:UpdateApplication")
beego.Router("/api/add-application", &controllers.ApiController{}, "POST:AddApplication")
beego.Router("/api/delete-application", &controllers.ApiController{}, "POST:DeleteApplication")
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider")
beego.Router("/api/get-global-providers", &controllers.ApiController{}, "GET:GetGlobalProviders")
beego.Router("/api/update-provider", &controllers.ApiController{}, "POST:UpdateProvider")
beego.Router("/api/add-provider", &controllers.ApiController{}, "POST:AddProvider")
beego.Router("/api/delete-provider", &controllers.ApiController{}, "POST:DeleteProvider")
beego.Router("/api/get-resources", &controllers.ApiController{}, "GET:GetResources")
beego.Router("/api/get-resource", &controllers.ApiController{}, "GET:GetResource")
beego.Router("/api/update-resource", &controllers.ApiController{}, "POST:UpdateResource")
beego.Router("/api/add-resource", &controllers.ApiController{}, "POST:AddResource")
beego.Router("/api/delete-resource", &controllers.ApiController{}, "POST:DeleteResource")
beego.Router("/api/upload-resource", &controllers.ApiController{}, "POST:UploadResource")
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-global-certs", &controllers.ApiController{}, "GET:GetGlobalCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
beego.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
beego.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
@ -107,12 +143,6 @@ func initAPI() {
beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission")
beego.Router("/api/upload-permissions", &controllers.ApiController{}, "POST:UploadPermissions")
beego.Router("/api/enforce", &controllers.ApiController{}, "POST:Enforce")
beego.Router("/api/batch-enforce", &controllers.ApiController{}, "POST:BatchEnforce")
beego.Router("/api/get-all-objects", &controllers.ApiController{}, "GET:GetAllObjects")
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
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")
@ -135,53 +165,11 @@ func initAPI() {
beego.Router("/api/add-enforcer", &controllers.ApiController{}, "POST:AddEnforcer")
beego.Router("/api/delete-enforcer", &controllers.ApiController{}, "POST:DeleteEnforcer")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-code", &controllers.ApiController{}, "POST:VerifyCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")
beego.Router("/api/get-ldap-users", &controllers.ApiController{}, "GET:GetLdapUsers")
beego.Router("/api/get-ldaps", &controllers.ApiController{}, "GET:GetLdaps")
beego.Router("/api/get-ldap", &controllers.ApiController{}, "GET:GetLdap")
beego.Router("/api/add-ldap", &controllers.ApiController{}, "POST:AddLdap")
beego.Router("/api/update-ldap", &controllers.ApiController{}, "POST:UpdateLdap")
beego.Router("/api/delete-ldap", &controllers.ApiController{}, "POST:DeleteLdap")
beego.Router("/api/sync-ldap-users", &controllers.ApiController{}, "POST:SyncLdapUsers")
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider")
beego.Router("/api/get-global-providers", &controllers.ApiController{}, "GET:GetGlobalProviders")
beego.Router("/api/update-provider", &controllers.ApiController{}, "POST:UpdateProvider")
beego.Router("/api/add-provider", &controllers.ApiController{}, "POST:AddProvider")
beego.Router("/api/delete-provider", &controllers.ApiController{}, "POST:DeleteProvider")
beego.Router("/api/get-applications", &controllers.ApiController{}, "GET:GetApplications")
beego.Router("/api/get-application", &controllers.ApiController{}, "GET:GetApplication")
beego.Router("/api/get-user-application", &controllers.ApiController{}, "GET:GetUserApplication")
beego.Router("/api/get-organization-applications", &controllers.ApiController{}, "GET:GetOrganizationApplications")
beego.Router("/api/update-application", &controllers.ApiController{}, "POST:UpdateApplication")
beego.Router("/api/add-application", &controllers.ApiController{}, "POST:AddApplication")
beego.Router("/api/delete-application", &controllers.ApiController{}, "POST:DeleteApplication")
beego.Router("/api/get-resources", &controllers.ApiController{}, "GET:GetResources")
beego.Router("/api/get-resource", &controllers.ApiController{}, "GET:GetResource")
beego.Router("/api/update-resource", &controllers.ApiController{}, "POST:UpdateResource")
beego.Router("/api/add-resource", &controllers.ApiController{}, "POST:AddResource")
beego.Router("/api/delete-resource", &controllers.ApiController{}, "POST:DeleteResource")
beego.Router("/api/upload-resource", &controllers.ApiController{}, "POST:UploadResource")
beego.Router("/api/get-tokens", &controllers.ApiController{}, "GET:GetTokens")
beego.Router("/api/get-token", &controllers.ApiController{}, "GET:GetToken")
beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken")
beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken")
beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken")
beego.Router("/api/enforce", &controllers.ApiController{}, "POST:Enforce")
beego.Router("/api/batch-enforce", &controllers.ApiController{}, "POST:BatchEnforce")
beego.Router("/api/get-all-objects", &controllers.ApiController{}, "GET:GetAllObjects")
beego.Router("/api/get-all-actions", &controllers.ApiController{}, "GET:GetAllActions")
beego.Router("/api/get-all-roles", &controllers.ApiController{}, "GET:GetAllRoles")
beego.Router("/api/get-sessions", &controllers.ApiController{}, "GET:GetSessions")
beego.Router("/api/get-session", &controllers.ApiController{}, "GET:GetSingleSession")
@ -190,43 +178,11 @@ func initAPI() {
beego.Router("/api/delete-session", &controllers.ApiController{}, "POST:DeleteSession")
beego.Router("/api/is-session-duplicated", &controllers.ApiController{}, "GET:IsSessionDuplicated")
beego.Router("/api/get-webhooks", &controllers.ApiController{}, "GET:GetWebhooks")
beego.Router("/api/get-webhook", &controllers.ApiController{}, "GET:GetWebhook")
beego.Router("/api/update-webhook", &controllers.ApiController{}, "POST:UpdateWebhook")
beego.Router("/api/add-webhook", &controllers.ApiController{}, "POST:AddWebhook")
beego.Router("/api/delete-webhook", &controllers.ApiController{}, "POST:DeleteWebhook")
beego.Router("/api/get-syncers", &controllers.ApiController{}, "GET:GetSyncers")
beego.Router("/api/get-syncer", &controllers.ApiController{}, "GET:GetSyncer")
beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer")
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
beego.Router("/api/run-syncer", &controllers.ApiController{}, "GET:RunSyncer")
beego.Router("/api/get-certs", &controllers.ApiController{}, "GET:GetCerts")
beego.Router("/api/get-global-certs", &controllers.ApiController{}, "GET:GetGlobalCerts")
beego.Router("/api/get-cert", &controllers.ApiController{}, "GET:GetCert")
beego.Router("/api/update-cert", &controllers.ApiController{}, "POST:UpdateCert")
beego.Router("/api/add-cert", &controllers.ApiController{}, "POST:AddCert")
beego.Router("/api/delete-cert", &controllers.ApiController{}, "POST:DeleteCert")
beego.Router("/api/get-subscriptions", &controllers.ApiController{}, "GET:GetSubscriptions")
beego.Router("/api/get-subscription", &controllers.ApiController{}, "GET:GetSubscription")
beego.Router("/api/update-subscription", &controllers.ApiController{}, "POST:UpdateSubscription")
beego.Router("/api/add-subscription", &controllers.ApiController{}, "POST:AddSubscription")
beego.Router("/api/delete-subscription", &controllers.ApiController{}, "POST:DeleteSubscription")
beego.Router("/api/get-plans", &controllers.ApiController{}, "GET:GetPlans")
beego.Router("/api/get-plan", &controllers.ApiController{}, "GET:GetPlan")
beego.Router("/api/update-plan", &controllers.ApiController{}, "POST:UpdatePlan")
beego.Router("/api/add-plan", &controllers.ApiController{}, "POST:AddPlan")
beego.Router("/api/delete-plan", &controllers.ApiController{}, "POST:DeletePlan")
beego.Router("/api/get-pricings", &controllers.ApiController{}, "GET:GetPricings")
beego.Router("/api/get-pricing", &controllers.ApiController{}, "GET:GetPricing")
beego.Router("/api/update-pricing", &controllers.ApiController{}, "POST:UpdatePricing")
beego.Router("/api/add-pricing", &controllers.ApiController{}, "POST:AddPricing")
beego.Router("/api/delete-pricing", &controllers.ApiController{}, "POST:DeletePricing")
beego.Router("/api/get-tokens", &controllers.ApiController{}, "GET:GetTokens")
beego.Router("/api/get-token", &controllers.ApiController{}, "GET:GetToken")
beego.Router("/api/update-token", &controllers.ApiController{}, "POST:UpdateToken")
beego.Router("/api/add-token", &controllers.ApiController{}, "POST:AddToken")
beego.Router("/api/delete-token", &controllers.ApiController{}, "POST:DeleteToken")
beego.Router("/api/get-products", &controllers.ApiController{}, "GET:GetProducts")
beego.Router("/api/get-product", &controllers.ApiController{}, "GET:GetProduct")
@ -244,6 +200,64 @@ func initAPI() {
beego.Router("/api/notify-payment/?:owner/?:payment", &controllers.ApiController{}, "POST:NotifyPayment")
beego.Router("/api/invoice-payment", &controllers.ApiController{}, "POST:InvoicePayment")
beego.Router("/api/get-plans", &controllers.ApiController{}, "GET:GetPlans")
beego.Router("/api/get-plan", &controllers.ApiController{}, "GET:GetPlan")
beego.Router("/api/update-plan", &controllers.ApiController{}, "POST:UpdatePlan")
beego.Router("/api/add-plan", &controllers.ApiController{}, "POST:AddPlan")
beego.Router("/api/delete-plan", &controllers.ApiController{}, "POST:DeletePlan")
beego.Router("/api/get-pricings", &controllers.ApiController{}, "GET:GetPricings")
beego.Router("/api/get-pricing", &controllers.ApiController{}, "GET:GetPricing")
beego.Router("/api/update-pricing", &controllers.ApiController{}, "POST:UpdatePricing")
beego.Router("/api/add-pricing", &controllers.ApiController{}, "POST:AddPricing")
beego.Router("/api/delete-pricing", &controllers.ApiController{}, "POST:DeletePricing")
beego.Router("/api/get-subscriptions", &controllers.ApiController{}, "GET:GetSubscriptions")
beego.Router("/api/get-subscription", &controllers.ApiController{}, "GET:GetSubscription")
beego.Router("/api/update-subscription", &controllers.ApiController{}, "POST:UpdateSubscription")
beego.Router("/api/add-subscription", &controllers.ApiController{}, "POST:AddSubscription")
beego.Router("/api/delete-subscription", &controllers.ApiController{}, "POST:DeleteSubscription")
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
beego.Router("/api/health", &controllers.ApiController{}, "GET:Health")
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
beego.Handler("/api/metrics", promhttp.Handler())
beego.Router("/api/get-syncers", &controllers.ApiController{}, "GET:GetSyncers")
beego.Router("/api/get-syncer", &controllers.ApiController{}, "GET:GetSyncer")
beego.Router("/api/update-syncer", &controllers.ApiController{}, "POST:UpdateSyncer")
beego.Router("/api/add-syncer", &controllers.ApiController{}, "POST:AddSyncer")
beego.Router("/api/delete-syncer", &controllers.ApiController{}, "POST:DeleteSyncer")
beego.Router("/api/run-syncer", &controllers.ApiController{}, "GET:RunSyncer")
beego.Router("/api/get-webhooks", &controllers.ApiController{}, "GET:GetWebhooks")
beego.Router("/api/get-webhook", &controllers.ApiController{}, "GET:GetWebhook")
beego.Router("/api/update-webhook", &controllers.ApiController{}, "POST:UpdateWebhook")
beego.Router("/api/add-webhook", &controllers.ApiController{}, "POST:AddWebhook")
beego.Router("/api/delete-webhook", &controllers.ApiController{}, "POST:DeleteWebhook")
beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword")
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-code", &controllers.ApiController{}, "POST:VerifyCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")
beego.Router("/api/get-ldap-users", &controllers.ApiController{}, "GET:GetLdapUsers")
beego.Router("/api/get-ldaps", &controllers.ApiController{}, "GET:GetLdaps")
beego.Router("/api/get-ldap", &controllers.ApiController{}, "GET:GetLdap")
beego.Router("/api/add-ldap", &controllers.ApiController{}, "POST:AddLdap")
beego.Router("/api/update-ldap", &controllers.ApiController{}, "POST:UpdateLdap")
beego.Router("/api/delete-ldap", &controllers.ApiController{}, "POST:DeleteLdap")
beego.Router("/api/sync-ldap-users", &controllers.ApiController{}, "POST:SyncLdapUsers")
beego.Router("/api/login/oauth/access_token", &controllers.ApiController{}, "POST:GetOAuthToken")
beego.Router("/api/login/oauth/refresh_token", &controllers.ApiController{}, "POST:RefreshToken")
beego.Router("/api/login/oauth/introspect", &controllers.ApiController{}, "POST:IntrospectToken")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
beego.Router("/api/send-notification", &controllers.ApiController{}, "POST:SendNotification")
@ -259,13 +273,6 @@ func initAPI() {
beego.Router("/api/delete-mfa", &controllers.ApiController{}, "POST:DeleteMfa")
beego.Router("/api/set-preferred-mfa", &controllers.ApiController{}, "POST:SetPreferredMfa")
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
beego.Router("/api/health", &controllers.ApiController{}, "GET:Health")
beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
beego.Handler("/api/metrics", promhttp.Handler())
beego.Router("/.well-known/openid-configuration", &controllers.RootController{}, "GET:GetOidcDiscovery")
beego.Router("/.well-known/jwks", &controllers.RootController{}, "*:GetJwks")

View File

@ -129,7 +129,7 @@ func StaticFilter(ctx *context.Context) {
path += urlPath
}
if !util.FileExist(path) {
if strings.Contains(path, "/../") || !util.FileExist(path) {
path = webBuildFolder + "/index.html"
}
if !util.FileExist(path) {

View File

@ -22,12 +22,13 @@ import (
func NewAwsS3StorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface {
sp := s3.New(&s3.Config{
AccessID: clientId,
AccessKey: clientSecret,
Region: region,
Bucket: bucket,
Endpoint: endpoint,
ACL: awss3.BucketCannedACLPublicRead,
AccessID: clientId,
AccessKey: clientSecret,
Region: region,
Bucket: bucket,
Endpoint: endpoint,
S3Endpoint: endpoint,
ACL: awss3.BucketCannedACLPublicRead,
})
return sp

View File

@ -19,13 +19,15 @@ import (
"github.com/casdoor/oss/googlecloud"
)
func NewGoogleCloudStorageProvider(clientId string, clientSecret string, bucket string, endpoint string) oss.StorageInterface {
sp, _ := googlecloud.New(&googlecloud.Config{
AccessID: clientId,
AccessKey: clientSecret,
Bucket: bucket,
Endpoint: endpoint,
func NewGoogleCloudStorageProvider(clientSecret string, bucket string, endpoint string) oss.StorageInterface {
sp, err := googlecloud.New(&googlecloud.Config{
ServiceAccountJson: clientSecret,
Bucket: bucket,
Endpoint: endpoint,
})
if err != nil {
panic(err)
}
return sp
}

View File

@ -33,7 +33,7 @@ func GetStorageProvider(providerType string, clientId string, clientSecret strin
case "Qiniu Cloud Kodo":
return NewQiniuCloudKodoStorageProvider(clientId, clientSecret, region, bucket, endpoint)
case "Google Cloud Storage":
return NewGoogleCloudStorageProvider(clientId, clientSecret, bucket, endpoint)
return NewGoogleCloudStorageProvider(clientSecret, bucket, endpoint)
}
return nil

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -72,7 +72,15 @@ func GetUrlPath(urlString string) string {
}
func GetUrlHost(urlString string) string {
u, _ := url.Parse(urlString)
if urlString == "" {
return ""
}
u, err := url.Parse(urlString)
if err != nil {
return err.Error()
}
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}

View File

@ -45,6 +45,19 @@ func ParseInt(s string) int {
return i
}
func ParseIntWithError(s string) (int, error) {
if s == "" {
return 0, fmt.Errorf("ParseIntWithError() error, empty string")
}
i, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return i, nil
}
func ParseFloat(s string) float64 {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
@ -309,3 +322,23 @@ func GetUsernameFromEmail(email string) string {
return tokens[0]
}
}
func StringToInterfaceArray(array []string) []interface{} {
var interfaceArray []interface{}
for _, v := range array {
interfaceArray = append(interfaceArray, v)
}
return interfaceArray
}
func StringToInterfaceArray2d(arrays [][]string) [][]interface{} {
var interfaceArrays [][]interface{}
for _, req := range arrays {
var interfaceArray []interface{}
for _, r := range req {
interfaceArray = append(interfaceArray, r)
}
interfaceArrays = append(interfaceArrays, interfaceArray)
}
return interfaceArrays
}

View File

@ -32,6 +32,7 @@
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.0.0",
"i18next": "^19.8.9",
"jwt-decode": "^4.0.0",
"libphonenumber-js": "^1.10.19",
"moment": "^2.29.1",
"qrcode.react": "^3.1.0",

View File

@ -15,62 +15,64 @@
import React, {Component} from "react";
import "./App.less";
import {Helmet} from "react-helmet";
import Dashboard from "./basic/Dashboard";
import ShortcutsPage from "./basic/ShortcutsPage";
import * as Setting from "./Setting";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {AppstoreTwoTone, BarsOutlined, DeploymentUnitOutlined, DollarTwoTone, DownOutlined, GithubOutlined, HomeTwoTone, InfoCircleFilled, LockTwoTone, LogoutOutlined, SafetyCertificateTwoTone, SettingOutlined, SettingTwoTone, ShareAltOutlined, WalletTwoTone} from "@ant-design/icons";
import {Alert, Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result, Tooltip} from "antd";
import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom";
import AccountPage from "./account/AccountPage";
import Dashboard from "./basic/Dashboard";
import ShortcutsPage from "./basic/ShortcutsPage";
import AppListPage from "./basic/AppListPage";
import OrganizationListPage from "./OrganizationListPage";
import OrganizationEditPage from "./OrganizationEditPage";
import GroupEditPage from "./GroupEdit";
import GroupListPage from "./GroupList";
import GroupTreePage from "./GroupTreePage";
import UserListPage from "./UserListPage";
import UserEditPage from "./UserEditPage";
import InvitationListPage from "./InvitationListPage";
import InvitationEditPage from "./InvitationEditPage";
import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage";
import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage";
import ResourceListPage from "./ResourceListPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import RoleListPage from "./RoleListPage";
import RoleEditPage from "./RoleEditPage";
import PermissionListPage from "./PermissionListPage";
import PermissionEditPage from "./PermissionEditPage";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import EnforcerEditPage from "./EnforcerEditPage";
import EnforcerListPage from "./EnforcerListPage";
import GroupTreePage from "./GroupTreePage";
import GroupEditPage from "./GroupEdit";
import GroupListPage from "./GroupList";
import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage";
import ApplicationListPage from "./ApplicationListPage";
import ApplicationEditPage from "./ApplicationEditPage";
import ResourceListPage from "./ResourceListPage";
import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage";
import SessionListPage from "./SessionListPage";
import TokenListPage from "./TokenListPage";
import TokenEditPage from "./TokenEditPage";
import WebhookListPage from "./WebhookListPage";
import WebhookEditPage from "./WebhookEditPage";
import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import CertListPage from "./CertListPage";
import CertEditPage from "./CertEditPage";
import SubscriptionListPage from "./SubscriptionListPage";
import SubscriptionEditPage from "./SubscriptionEditPage";
import PricingListPage from "./PricingListPage";
import PricingEditPage from "./PricingEditPage";
import PlanListPage from "./PlanListPage";
import PlanEditPage from "./PlanEditPage";
import ProductListPage from "./ProductListPage";
import ProductEditPage from "./ProductEditPage";
import ProductBuyPage from "./ProductBuyPage";
import PaymentListPage from "./PaymentListPage";
import PaymentEditPage from "./PaymentEditPage";
import PaymentResultPage from "./PaymentResultPage";
import ModelListPage from "./ModelListPage";
import ModelEditPage from "./ModelEditPage";
import AdapterListPage from "./AdapterListPage";
import AdapterEditPage from "./AdapterEditPage";
import SessionListPage from "./SessionListPage";
import MfaSetupPage from "./auth/MfaSetupPage";
import PricingListPage from "./PricingListPage";
import PricingEditPage from "./PricingEditPage";
import PlanListPage from "./PlanListPage";
import PlanEditPage from "./PlanEditPage";
import SubscriptionListPage from "./SubscriptionListPage";
import SubscriptionEditPage from "./SubscriptionEditPage";
import SystemInfo from "./SystemInfo";
import AccountPage from "./account/AccountPage";
import AppListPage from "./basic/AppListPage";
import SyncerListPage from "./SyncerListPage";
import SyncerEditPage from "./SyncerEditPage";
import WebhookListPage from "./WebhookListPage";
import WebhookEditPage from "./WebhookEditPage";
import LdapEditPage from "./LdapEditPage";
import LdapSyncPage from "./LdapSyncPage";
import MfaSetupPage from "./auth/MfaSetupPage";
import CustomGithubCorner from "./common/CustomGithubCorner";
import * as Conf from "./Conf";
@ -153,7 +155,7 @@ class App extends Component {
});
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {
this.setState({selectedMenuKey: "/home"});
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/users") || uri.includes("/groups")) {
} else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
this.setState({selectedMenuKey: "/orgs"});
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) {
this.setState({selectedMenuKey: "/identity"});
@ -434,6 +436,7 @@ class App extends Component {
Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>, "/organizations"),
Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>, "/groups"),
Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>, "/users"),
Setting.getItem(<Link to="/invitations">{i18next.t("general:Invitations")}</Link>, "/invitations"),
]));
res.push(Setting.getItem(<Link style={{color: "black"}} to="/applications">{i18next.t("general:Identity")}</Link>, "/identity", <LockTwoTone />, [
@ -514,48 +517,49 @@ class App extends Component {
<Route exact path="/groups/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupEditPage account={this.state.account} {...props} />)} />
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} />
<Route exact path="/invitations" render={(props) => this.renderLoginIfNotLoggedIn(<InvitationListPage account={this.state.account} {...props} />)} />
<Route exact path="/invitations/:organizationName/:invitationName" render={(props) => this.renderLoginIfNotLoggedIn(<InvitationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/applications" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationListPage account={this.state.account} {...props} />)} />
<Route exact path="/applications/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/:providerName" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderEditPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:organizationName/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage 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/: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="/enforcers" render={(props) => this.renderLoginIfNotLoggedIn(<EnforcerListPage account={this.state.account} {...props} />)} />
<Route exact path="/enforcers/:organizationName/:enforcerName" render={(props) => this.renderLoginIfNotLoggedIn(<EnforcerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterListPage account={this.state.account} {...props} />)} />
<Route exact path="/adapters/:organizationName/:adapterName" render={(props) => this.renderLoginIfNotLoggedIn(<AdapterEditPage account={this.state.account} {...props} />)} />
<Route exact path="/providers" render={(props) => this.renderLoginIfNotLoggedIn(<ProviderListPage account={this.state.account} {...props} />)} />
<Route exact path="/providers/:organizationName/: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/:organizationName/:applicationName" render={(props) => this.renderLoginIfNotLoggedIn(<ApplicationEditPage account={this.state.account} {...props} />)} />
<Route exact path="/resources" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceListPage account={this.state.account} {...props} />)} />
{/* <Route exact path="/resources/:resourceName" render={(props) => this.renderLoginIfNotLoggedIn(<ResourceEditPage account={this.state.account} {...props} />)}/>*/}
<Route exact path="/ldap/:organizationName/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/sync/:organizationName/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} />
<Route exact path="/enforcers" render={(props) => this.renderLoginIfNotLoggedIn(<EnforcerListPage account={this.state.account} {...props} />)} />
<Route exact path="/enforcers/:organizationName/:enforcerName" render={(props) => this.renderLoginIfNotLoggedIn(<EnforcerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/sessions" render={(props) => this.renderLoginIfNotLoggedIn(<SessionListPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)} />
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/certs" render={(props) => this.renderLoginIfNotLoggedIn(<CertListPage account={this.state.account} {...props} />)} />
<Route exact path="/certs/:organizationName/:certName" render={(props) => this.renderLoginIfNotLoggedIn(<CertEditPage account={this.state.account} {...props} />)} />
<Route exact path="/plans" render={(props) => this.renderLoginIfNotLoggedIn(<PlanListPage account={this.state.account} {...props} />)} />
<Route exact path="/plans/:organizationName/:planName" render={(props) => this.renderLoginIfNotLoggedIn(<PlanEditPage account={this.state.account} {...props} />)} />
<Route exact path="/pricings" render={(props) => this.renderLoginIfNotLoggedIn(<PricingListPage account={this.state.account} {...props} />)} />
<Route exact path="/pricings/:organizationName/:pricingName" render={(props) => this.renderLoginIfNotLoggedIn(<PricingEditPage account={this.state.account} {...props} />)} />
<Route exact path="/subscriptions" render={(props) => this.renderLoginIfNotLoggedIn(<SubscriptionListPage account={this.state.account} {...props} />)} />
<Route exact path="/subscriptions/:organizationName/:subscriptionName" render={(props) => this.renderLoginIfNotLoggedIn(<SubscriptionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products" render={(props) => this.renderLoginIfNotLoggedIn(<ProductListPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:organizationName/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:organizationName/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:organizationName/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:organizationName/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/plans" render={(props) => this.renderLoginIfNotLoggedIn(<PlanListPage account={this.state.account} {...props} />)} />
<Route exact path="/plans/:organizationName/:planName" render={(props) => this.renderLoginIfNotLoggedIn(<PlanEditPage account={this.state.account} {...props} />)} />
<Route exact path="/pricings" render={(props) => this.renderLoginIfNotLoggedIn(<PricingListPage account={this.state.account} {...props} />)} />
<Route exact path="/pricings/:organizationName/:pricingName" render={(props) => this.renderLoginIfNotLoggedIn(<PricingEditPage account={this.state.account} {...props} />)} />
<Route exact path="/subscriptions" render={(props) => this.renderLoginIfNotLoggedIn(<SubscriptionListPage account={this.state.account} {...props} />)} />
<Route exact path="/subscriptions/:organizationName/:subscriptionName" render={(props) => this.renderLoginIfNotLoggedIn(<SubscriptionEditPage account={this.state.account} {...props} />)} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route exact path="/syncers" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerListPage account={this.state.account} {...props} />)} />
<Route exact path="/syncers/:syncerName" render={(props) => this.renderLoginIfNotLoggedIn(<SyncerEditPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)} />
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/:organizationName/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapEditPage account={this.state.account} {...props} />)} />
<Route exact path="/ldap/sync/:organizationName/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)} />
<Route exact path="/mfa/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} onfinish={() => this.setState({requiredEnableMfa: false})} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />
<Route exact path="/sysinfo" render={(props) => this.renderLoginIfNotLoggedIn(<SystemInfo account={this.state.account} {...props} />)} />
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} />} />
</Switch>

View File

@ -13,7 +13,7 @@
// limitations under the License.
import React from "react";
import {Button, Card, Col, ConfigProvider, Input, List, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
import {Button, Card, Col, ConfigProvider, Input, InputNumber, List, Popover, Radio, Result, Row, Select, Space, Switch, Upload} from "antd";
import {CopyOutlined, LinkOutlined, UploadOutlined} from "@ant-design/icons";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as CertBackend from "./backend/CertBackend";
@ -199,7 +199,7 @@ class ApplicationEditPage extends React.Component {
}
parseApplicationField(key, value) {
if (["expireInHours", "refreshExpireInHours", "offset"].includes(key)) {
if (["offset"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
@ -394,8 +394,8 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("application:Token expire"), i18next.t("application:Token expire - Tooltip"))} :
</Col>
<Col span={22} >
<Input style={{width: "150px"}} value={this.state.application.expireInHours} suffix="Hours" onChange={e => {
this.updateApplicationField("expireInHours", e.target.value);
<InputNumber style={{width: "150px"}} value={this.state.application.expireInHours} min={1} step={1} precision={0} addonAfter="Hours" onChange={value => {
this.updateApplicationField("expireInHours", value);
}} />
</Col>
</Row>
@ -404,8 +404,28 @@ class ApplicationEditPage extends React.Component {
{Setting.getLabel(i18next.t("application:Refresh token expire"), i18next.t("application:Refresh token expire - Tooltip"))} :
</Col>
<Col span={22} >
<Input style={{width: "150px"}} value={this.state.application.refreshExpireInHours} suffix="Hours" onChange={e => {
this.updateApplicationField("refreshExpireInHours", e.target.value);
<InputNumber style={{width: "150px"}} value={this.state.application.refreshExpireInHours} min={1} step={1} precision={0} addonAfter="Hours" onChange={value => {
this.updateApplicationField("refreshExpireInHours", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Failed signin limit"), i18next.t("application:Failed signin limit - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber style={{width: "150px"}} value={this.state.application.failedSigninLimit} min={1} step={1} precision={0} addonAfter="Times" onChange={value => {
this.updateApplicationField("failedSigninLimit", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Failed signin frozen time"), i18next.t("application:Failed signin frozen time - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber style={{width: "150px"}} value={this.state.application.failedSigninfrozenTime} min={1} step={1} precision={0} addonAfter="Minutes" onChange={value => {
this.updateApplicationField("failedSigninfrozenTime", value);
}} />
</Col>
</Row>
@ -676,7 +696,7 @@ class ApplicationEditPage extends React.Component {
<br />
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}/api/saml/metadata?application=admin/${encodeURIComponent(this.state.applicationName)}`);
Setting.showMessage("success", i18next.t("application:SAML metadata URL copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("application:Copy SAML metadata URL")}
@ -885,7 +905,7 @@ class ApplicationEditPage extends React.Component {
<Space>
<Button icon={<CopyOutlined />} onClick={() => {
copy(item.code);
Setting.showMessage("success", i18next.t("application:Invitation code copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}
}>
{i18next.t("general:Copy")}
@ -939,7 +959,7 @@ class ApplicationEditPage extends React.Component {
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signUpUrl}`);
Setting.showMessage("success", i18next.t("application:Signup page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("application:Copy signup page URL")}
@ -971,7 +991,7 @@ class ApplicationEditPage extends React.Component {
<Col span={previewGrid}>
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${signInUrl}`);
Setting.showMessage("success", i18next.t("application:Signin page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("application:Copy signin page URL")}
@ -1004,7 +1024,7 @@ class ApplicationEditPage extends React.Component {
<Col span={previewGrid}>
<Button style={{marginBottom: "10px"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${promptUrl}`);
Setting.showMessage("success", i18next.t("application:Prompt page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("application:Copy prompt page URL")}

View File

@ -230,7 +230,7 @@ class CertEditPage extends React.Component {
<Col span={editorWidth} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} disabled={this.state.cert.certificate === ""} onClick={() => {
copy(this.state.cert.certificate);
Setting.showMessage("success", i18next.t("cert:Certificate copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy certificate")}
@ -253,7 +253,7 @@ class CertEditPage extends React.Component {
<Col span={editorWidth} >
<Button style={{marginRight: "10px", marginBottom: "10px"}} disabled={this.state.cert.privateKey === ""} onClick={() => {
copy(this.state.cert.privateKey);
Setting.showMessage("success", i18next.t("cert:Private key copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("cert:Copy private key")}

View File

@ -0,0 +1,282 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, InputNumber, Row, Select} from "antd";
import * as InvitationBackend from "./backend/InvitationBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
const {Option} = Select;
class InvitationEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
invitationName: props.match.params.invitationName,
invitation: null,
organizations: [],
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getInvitation();
this.getOrganizations();
this.getApplicationsByOrganization(this.state.organizationName);
}
getInvitation() {
InvitationBackend.getInvitation(this.state.organizationName, this.state.invitationName)
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
this.setState({
invitation: res.data,
});
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: res.data || [],
});
});
}
getApplicationsByOrganization(organizationName) {
ApplicationBackend.getApplicationsByOrganization("admin", organizationName)
.then((res) => {
this.setState({
applications: res.data || [],
});
});
}
parseInvitationField(key, value) {
if ([""].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateInvitationField(key, value) {
value = this.parseInvitationField(key, value);
const invitation = this.state.invitation;
invitation[key] = value;
this.setState({
invitation: invitation,
});
}
renderInvitation() {
const isCreatedByPlan = this.state.invitation.tag === "auto_created_invitation_for_plan";
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{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%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.invitation.owner} onChange={(value => {this.updateInvitationField("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.invitation.name} disabled={isCreatedByPlan} onChange={e => {
this.updateInvitationField("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.invitation.displayName} onChange={e => {
this.updateInvitationField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Code"), i18next.t("invitation:Code - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.invitation.code} onChange={e => {
this.updateInvitationField("code", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber min={0} value={this.state.invitation.quota} onChange={value => {
this.updateInvitationField("quota", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Used count"), i18next.t("invitation:Used count - Tooltip"))} :
</Col>
<Col span={22} >
<InputNumber min={0} max={this.state.invitation.quota} value={this.state.invitation.usedCount} onChange={value => {
this.updateInvitationField("usedCount", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.invitation.application}
onChange={(value => {this.updateInvitationField("application", value);})}
options={this.state.applications.map((application) => Setting.getOption(application.name, application.name))
} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("signup:Username"), i18next.t("signup:Username - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.invitation.username} onChange={e => {
this.updateInvitationField("username", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Email"), i18next.t("general:Email - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.invitation.email} onChange={e => {
this.updateInvitationField("email", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Phone"), i18next.t("general:Phone - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.invitation.phone} onChange={e => {
this.updateInvitationField("phone", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:State"), i18next.t("general:State - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.invitation.state} onChange={(value => {
this.updateInvitationField("state", value);
})}
options={[
{value: "Active", name: i18next.t("subscription:Active")},
{value: "Suspended", name: i18next.t("subscription:Suspended")},
].map((item) => Setting.getOption(item.name, item.value))}
/>
</Col>
</Row>
</Card>
);
}
submitInvitationEdit(exitAfterSave) {
const invitation = Setting.deepCopy(this.state.invitation);
InvitationBackend.updateInvitation(this.state.organizationName, this.state.invitationName, invitation)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
invitationName: this.state.invitation.name,
});
if (exitAfterSave) {
this.props.history.push("/invitations");
} else {
this.props.history.push(`/invitations/${this.state.invitation.owner}/${this.state.invitation.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
this.updateInvitationField("name", this.state.invitationName);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteInvitation() {
InvitationBackend.deleteInvitation(this.state.invitation)
.then((res) => {
if (res.status === "ok") {
this.props.history.push("/invitations");
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
render() {
return (
<div>
{
this.state.invitation !== null ? this.renderInvitation() : null
}
<div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
</div>
</div>
);
}
}
export default InvitationEditPage;

View File

@ -0,0 +1,310 @@
// Copyright 2023 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, Table} from "antd";
import {MinusCircleOutlined, SyncOutlined} from "@ant-design/icons";
import moment from "moment";
import * as Setting from "./Setting";
import * as InvitationBackend from "./backend/InvitationBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
import copy from "copy-to-clipboard";
class InvitationListPage extends BaseListPage {
newInvitation() {
const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account);
return {
owner: owner,
name: `invitation_${randomName}`,
createdTime: moment().format(),
updatedTime: moment().format(),
displayName: `New Invitation - ${randomName}`,
code: Math.random().toString(36).slice(-10),
quota: 1,
usedCount: 0,
application: "",
username: "",
email: "",
phone: "",
signupGroup: "",
state: "Active",
};
}
addInvitation() {
const newInvitation = this.newInvitation();
InvitationBackend.addInvitation(newInvitation)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/invitations/${newInvitation.owner}/${newInvitation.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added"));
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteInvitation(i) {
InvitationBackend.deleteInvitation(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
renderTable(invitations) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "140px",
fixed: "left",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/invitations/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "150px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${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:Updated time"),
dataIndex: "updatedTime",
key: "updatedTime",
width: "160px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
width: "170px",
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("invitation:Code"),
dataIndex: "code",
key: "code",
width: "160px",
sorter: true,
...this.getColumnSearchProps("code"),
},
{
title: i18next.t("invitation:Quota"),
dataIndex: "quota",
key: "quota",
width: "120px",
sorter: true,
...this.getColumnSearchProps("quota"),
},
{
title: i18next.t("invitation:Used count"),
dataIndex: "usedCount",
key: "usedCount",
width: "130px",
sorter: true,
...this.getColumnSearchProps("usedCount"),
},
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "170px",
sorter: true,
...this.getColumnSearchProps("application"),
render: (text, record, index) => {
return (
<Link to={`/applications/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Email"),
dataIndex: "email",
key: "email",
width: "160px",
sorter: true,
...this.getColumnSearchProps("email"),
render: (text, record, index) => {
return (
<a href={`mailto:${text}`}>
{text}
</a>
);
},
},
{
title: i18next.t("general:Phone"),
dataIndex: "phone",
key: "phone",
width: "120px",
sorter: true,
...this.getColumnSearchProps("phone"),
},
{
title: i18next.t("general:State"),
dataIndex: "state",
key: "state",
width: "120px",
sorter: true,
...this.getColumnSearchProps("state"),
render: (text, record, index) => {
switch (text) {
case "Active":
return Setting.getTag("success", i18next.t("subscription:Active"), <SyncOutlined spin />);
case "Suspended":
return Setting.getTag("default", i18next.t("subscription:Suspended"), <MinusCircleOutlined />);
default:
return null;
}
},
},
{
title: i18next.t("general:Action"),
dataIndex: "",
key: "op",
width: "350px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => {
copy(`${window.location.origin}/login/${record.owner}?invitation_code=${record.code}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteInvitation(index)}
>
</PopconfirmModal>
</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={invitations} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Invitations")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addInvitation.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;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (params.type !== undefined && params.type !== null) {
field = "type";
value = params.type;
}
this.setState({loading: true});
InvitationBackend.getInvitations(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({
loading: false,
});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
if (Setting.isResponseDenied(res)) {
this.setState({
isAuthorized: false,
});
} else {
Setting.showMessage("error", res.msg);
}
}
});
};
}
export default InvitationListPage;

View File

@ -303,7 +303,7 @@ class PermissionEditPage extends React.Component {
{Setting.getLabel(i18next.t("role:Sub roles"), i18next.t("role:Sub roles - Tooltip"))} :
</Col>
<Col span={22} >
<Select disabled={!this.hasRoleDefinition(this.state.model)} virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.roles}
<Select disabled={!this.hasRoleDefinition(this.state.model)} placeholder={this.hasRoleDefinition(this.state.model) ? "" : "This field is disabled because the model is empty or it doesn't support RBAC (in another word, doesn't contain [role_definition])"} virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.permission.roles}
onChange={(value => {this.updatePermissionField("roles", value);})}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
@ -323,7 +323,7 @@ class PermissionEditPage extends React.Component {
})}
options={[
Setting.getOption(i18next.t("organization:All"), "*"),
...this.state.permission.domains.map((domain) => Setting.getOption(domain, domain)),
...this.state.permission.domains.filter(domain => domain !== "*").map((domain) => Setting.getOption(domain, domain)),
]}
/>
</Col>

View File

@ -32,7 +32,6 @@ class PricingEditPage extends React.Component {
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
pricingName: props.match.params.pricingName,
organizations: [],
application: null,
applications: [],
pricing: null,
plans: [],
@ -287,7 +286,7 @@ class PricingEditPage extends React.Component {
<Col>
<Button style={{marginBottom: "10px", marginTop: Setting.isMobile() ? "15px" : "0"}} type="primary" shape="round" icon={<CopyOutlined />} onClick={() => {
copy(`${window.location.origin}${pricingUrl}`);
Setting.showMessage("success", i18next.t("pricing:pricing page URL copied to clipboard successfully, please paste it into the incognito window or another browser"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("pricing:Copy pricing page URL")}

View File

@ -29,6 +29,14 @@ import {CaptchaPreview} from "./common/CaptchaPreview";
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
import * as Web3Auth from "./auth/Web3Auth";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/theme/material-darker.css");
require("codemirror/mode/htmlmixed/htmlmixed");
require("codemirror/mode/xml/xml");
require("codemirror/mode/css/css");
const {Option} = Select;
const {TextArea} = Input;
@ -197,6 +205,12 @@ class ProviderEditPage extends React.Component {
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
case "Storage":
if (provider.type === "Google Cloud Storage") {
return Setting.getLabel(i18next.t("provider:Service account JSON"), i18next.t("provider:Service account JSON - Tooltip"));
} else {
return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip"));
}
case "Email":
if (provider.type === "Azure ACS") {
return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip"));
@ -305,6 +319,9 @@ class ProviderEditPage extends React.Component {
} else if (provider.type === "Infoflow") {
text = i18next.t("provider:Agent ID");
tooltip = i18next.t("provider:Agent ID - Tooltip");
} else if (provider.type === "AzureADB2C") {
text = i18next.t("provider:User flow");
tooltip = i18next.t("provider:User flow - Tooltip");
}
} else if (provider.category === "SMS") {
if (provider.type === "Twilio SMS" || provider.type === "Azure ACS") {
@ -471,7 +488,7 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("port", 465);
this.updateProviderField("disableSsl", false);
this.updateProviderField("title", "Casdoor Verification Code");
this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.");
this.updateProviderField("content", Setting.getDefaultHtmlEmailContent());
this.updateProviderField("receiver", this.props.account.email);
} else if (value === "SMS") {
this.updateProviderField("type", "Twilio SMS");
@ -521,6 +538,13 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("scopes", "openid profile email");
this.updateProviderField("customTokenUrl", "https://door.casdoor.com/api/login/oauth/access_token");
this.updateProviderField("customUserInfoUrl", "https://door.casdoor.com/api/userinfo");
} else if (value === "Custom HTTP SMS") {
this.updateProviderField("endpoint", "https://example.com/send-custom-http-sms");
this.updateProviderField("method", "GET");
this.updateProviderField("title", "code");
} else if (value === "Custom HTTP Email") {
this.updateProviderField("endpoint", "https://example.com/send-custom-http-email");
this.updateProviderField("method", "POST");
} else if (value === "Custom HTTP") {
this.updateProviderField("method", "GET");
this.updateProviderField("title", "");
@ -668,9 +692,11 @@ class ProviderEditPage extends React.Component {
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") ||
(this.state.provider.category === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP")) ? null : (
<React.Fragment>
{
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||
(this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Line" || this.state.provider.type === "Telegram" || this.state.provider.type === "Bark" || this.state.provider.type === "Discord" || this.state.provider.type === "Slack" || this.state.provider.type === "Pushbullet" || this.state.provider.type === "Pushover" || this.state.provider.type === "Lark" || this.state.provider.type === "Microsoft Teams")) ? null : (
<Row style={{marginTop: "20px"}} >
@ -743,7 +769,7 @@ class ProviderEditPage extends React.Component {
)
}
{
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
this.state.provider.type !== "ADFS" && this.state.provider.type !== "AzureAD" && this.state.provider.type !== "AzureADB2C" && this.state.provider.type !== "Casdoor" && this.state.provider.type !== "Okta" ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -756,7 +782,7 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{this.state.provider.category === "Storage" ? (
{this.state.provider.category === "Storage" || ["Custom HTTP SMS", "Custom HTTP Email"].includes(this.state.provider.type) ? (
<div>
{["Local File System"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
@ -770,7 +796,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Local File System", "MinIO", "Tencent Cloud COS", "Google Cloud Storage", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Endpoint (Intranet)"), i18next.t("provider:Region endpoint for Intranet"))} :
@ -782,7 +808,7 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
{["Local File System"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS", "Local File System"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Bucket"), i18next.t("provider:Bucket - Tooltip"))} :
@ -794,17 +820,19 @@ class ProviderEditPage extends React.Component {
</Col>
</Row>
)}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.pathPrefix} onChange={e => {
this.updateProviderField("pathPrefix", e.target.value);
}} />
</Col>
</Row>
{["MinIO", "Google Cloud Storage", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? null : (
{["Custom HTTP SMS"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Path prefix"), i18next.t("provider:Path prefix - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.pathPrefix} onChange={e => {
this.updateProviderField("pathPrefix", e.target.value);
}} />
</Col>
</Row>
)}
{["Custom HTTP SMS", "MinIO", "Google Cloud Storage", "Qiniu Cloud Kodo"].includes(this.state.provider.type) ? null : (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("provider:Domain"), i18next.t("provider:Domain - Tooltip"))} :
@ -946,22 +974,47 @@ class ProviderEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} :
</Col>
<Col span={22} >
<TextArea autoSize={{minRows: 3, maxRows: 100}} value={this.state.provider.content} onChange={e => {
this.updateProviderField("content", e.target.value);
}} />
<Row style={{marginTop: "20px"}} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => this.updateProviderField("content", "You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.")} >
{i18next.t("provider:Reset to Default Text")}
</Button>
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => this.updateProviderField("content", Setting.getDefaultHtmlEmailContent())} >
{i18next.t("provider:Reset to Default HTML")}
</Button>
</Row>
<Row>
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{height: "300px", margin: "10px"}}>
<CodeMirror
value={this.state.provider.content}
options={{mode: "htmlmixed", theme: "material-darker"}}
onBeforeChange={(editor, data, value) => {
this.updateProviderField("content", value);
}}
/>
</div>
</Col>
<Col span={1} />
<Col span={Setting.isMobile() ? 22 : 11}>
<div style={{margin: "10px"}}>
<div dangerouslySetInnerHTML={{__html: this.state.provider.content.replace("%s", "123456").replace("%{user.friendlyName}", Setting.getFriendlyUserName(this.props.account))}} />
</div>
</Col>
</Row>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Row style={{marginTop: "20px"}}>
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Test Email"), i18next.t("provider:Test Email - Tooltip"))} :
</Col>
<Col span={4} >
<Input value={this.state.provider.receiver} placeholder = {i18next.t("user:Input your email")} onChange={e => {
this.updateProviderField("receiver", e.target.value);
}} />
<Col span={4}>
<Input value={this.state.provider.receiver} placeholder={i18next.t("user:Input your email")}
onChange={e => {
this.updateProviderField("receiver", e.target.value);
}} />
</Col>
{["Azure ACS"].includes(this.state.provider.type) ? null : (
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
{i18next.t("provider:Test SMTP Connection")}
</Button>
)}
@ -974,7 +1027,7 @@ class ProviderEditPage extends React.Component {
</React.Fragment>
) : this.state.provider.category === "SMS" ? (
<React.Fragment>
{["Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
{["Custom HTTP SMS", "Twilio SMS", "Amazon SNS", "Azure ACS", "Msg91 SMS", "Infobip SMS"].includes(this.state.provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -988,7 +1041,7 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{["Infobip SMS"].includes(this.state.provider.type) ?
{["Custom HTTP SMS", "Infobip SMS"].includes(this.state.provider.type) ?
null :
(<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
@ -1002,6 +1055,39 @@ class ProviderEditPage extends React.Component {
</Row>
)
}
{
!["Custom HTTP SMS", "Custom HTTP Email"].includes(this.state.provider.type) ? null : (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}>
{Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.method} onChange={value => {
this.updateProviderField("method", value);
}}>
{
[
{id: "GET", name: "GET"},
{id: "POST", name: "POST"},
].map((method, index) => <Option key={index} value={method.id}>{method.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:Parameter"), i18next.t("provider:Parameter - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.provider.title} onChange={e => {
this.updateProviderField("title", e.target.value);
}} />
</Col>
</Row>
</React.Fragment>
)
}
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("provider:SMS Test"), i18next.t("provider:SMS Test - Tooltip"))} :
@ -1026,7 +1112,7 @@ class ProviderEditPage extends React.Component {
</Col>
<Col span={2} >
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
disabled={!Setting.isValidPhone(this.state.provider.receiver)}
disabled={!Setting.isValidPhone(this.state.provider.receiver) && (this.state.provider.type !== "Custom HTTP SMS" || this.state.provider.endpoint === "")}
onClick={() => ProviderEditTestSms.sendTestSms(this.state.provider, "+" + Setting.getCountryCode(this.state.provider.content) + this.state.provider.receiver)} >
{i18next.t("provider:Send Testing SMS")}
</Button>
@ -1110,7 +1196,7 @@ class ProviderEditPage extends React.Component {
<Col span={1}>
<Button type="primary" onClick={() => {
copy(`${authConfig.serverUrl}/api/acs`);
Setting.showMessage("success", i18next.t("provider:Link copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("provider:Copy")}
</Button>
@ -1126,7 +1212,7 @@ class ProviderEditPage extends React.Component {
<Col span={1}>
<Button type="primary" onClick={() => {
copy(`${authConfig.serverUrl}/api/acs`);
Setting.showMessage("success", i18next.t("provider:Link copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("provider:Copy")}
</Button>
@ -1155,7 +1241,7 @@ class ProviderEditPage extends React.Component {
(this.state.provider.type === "Alipay") ? (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Root Cert"), i18next.t("general:Root Cert - Tooltip"))} :
{Setting.getLabel(i18next.t("general:Root cert"), i18next.t("general:Root cert - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.metadata} onChange={(value => {this.updateProviderField("metadata", value);})}>

View File

@ -248,7 +248,7 @@ class ResourceListPage extends BaseListPage {
<div>
<Button onClick={() => {
copy(record.url);
Setting.showMessage("success", i18next.t("provider:Link copied to clipboard successfully"));
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}
>
{i18next.t("resource:Copy Link")}

View File

@ -143,6 +143,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_msg91.ico`,
url: "https://control.msg91.com/app/",
},
"Custom HTTP SMS": {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://casdoor.org/docs/provider/sms/overview",
},
"Mock SMS": {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "",
@ -165,6 +169,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
},
"Custom HTTP Email": {
logo: `${StaticBaseUrl}/img/social_default.png`,
url: "https://casdoor.org/docs/provider/email/overview",
},
},
Storage: {
"Local File System": {
@ -916,7 +924,8 @@ export function getProviderTypeOptions(category) {
{id: "Casdoor", name: "Casdoor"},
{id: "Infoflow", name: "Infoflow"},
{id: "Apple", name: "Apple"},
{id: "AzureAD", name: "AzureAD"},
{id: "AzureAD", name: "Azure AD"},
{id: "AzureADB2C", name: "Azure AD B2C"},
{id: "Slack", name: "Slack"},
{id: "Steam", name: "Steam"},
{id: "Bilibili", name: "Bilibili"},
@ -981,6 +990,7 @@ export function getProviderTypeOptions(category) {
{id: "SUBMAIL", name: "SUBMAIL"},
{id: "Mailtrap", name: "Mailtrap"},
{id: "Azure ACS", name: "Azure ACS"},
{id: "Custom HTTP Email", name: "Custom HTTP Email"},
]
);
} else if (category === "SMS") {
@ -989,6 +999,8 @@ export function getProviderTypeOptions(category) {
{id: "Aliyun SMS", name: "Alibaba Cloud SMS"},
{id: "Amazon SNS", name: "Amazon SNS"},
{id: "Azure ACS", name: "Azure ACS"},
{id: "Custom HTTP SMS", name: "Custom HTTP SMS"},
{id: "Mock SMS", name: "Mock SMS"},
{id: "Infobip SMS", name: "Infobip SMS"},
{id: "Tencent Cloud SMS", name: "Tencent Cloud SMS"},
{id: "Baidu Cloud SMS", name: "Baidu Cloud SMS"},
@ -1393,3 +1405,53 @@ export function getCurrencySymbol(currency) {
return currency;
}
}
export function getFriendlyUserName(account) {
if (account.firstName !== "" && account.lastName !== "") {
return `${account.firstName}, ${account.lastName}`;
} else if (account.displayName !== "") {
return account.displayName;
} else if (account.name !== "") {
return account.name;
} else {
return account.id;
}
}
export function getDefaultHtmlEmailContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verification Code Email</title>
<style>
body { font-family: Arial, sans-serif; }
.email-container { width: 600px; margin: 0 auto; }
.header { text-align: center; }
.code { font-size: 24px; margin: 20px 0; text-align: center; }
.footer { font-size: 12px; text-align: center; margin-top: 50px; }
.footer a { color: #000; text-decoration: none; }
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h3>Casbin Organization</h3>
<img src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png" alt="Casdoor Logo" width="300">
</div>
<p><strong>%{user.friendlyName}</strong>, here is your verification code</p>
<p>Use this code for your transaction. It's valid for 5 minutes</p>
<div class="code">
%s
</div>
<p>Thanks</p>
<p>Casbin Team</p>
<hr>
<div class="footer">
<p>Casdoor is a brand operated by Casbin organization. For more info please refer to <a href="https://casdoor.org">https://casdoor.org</a></p>
</div>
</div>
</body>
</html>`;
}

View File

@ -280,12 +280,12 @@ class SubscriptionEditPage extends React.Component {
this.updateSubscriptionField("state", value);
})}
options={[
{value: "Pending", name: i18next.t("permission:Pending")},
{value: "Active", name: i18next.t("permission:Active")},
{value: "Upcoming", name: i18next.t("permission:Upcoming")},
{value: "Expired", name: i18next.t("permission:Expired")},
{value: "Error", name: i18next.t("permission:Error")},
{value: "Suspended", name: i18next.t("permission:Suspended")},
{value: "Pending", name: i18next.t("subscription:Pending")},
{value: "Active", name: i18next.t("subscription:Active")},
{value: "Upcoming", name: i18next.t("subscription:Upcoming")},
{value: "Expired", name: i18next.t("subscription:Expired")},
{value: "Error", name: i18next.t("subscription:Error")},
{value: "Suspended", name: i18next.t("subscription:Suspended")},
].map((item) => Setting.getOption(item.name, item.value))}
/>
</Col>

View File

@ -201,17 +201,17 @@ class SubscriptionListPage extends BaseListPage {
render: (text, record, index) => {
switch (text) {
case "Pending":
return Setting.getTag("processing", i18next.t("permission:Pending"), <ExclamationCircleOutlined />);
return Setting.getTag("processing", i18next.t("subscription:Pending"), <ExclamationCircleOutlined />);
case "Active":
return Setting.getTag("success", i18next.t("permission:Active"), <SyncOutlined spin />);
return Setting.getTag("success", i18next.t("subscription:Active"), <SyncOutlined spin />);
case "Upcoming":
return Setting.getTag("warning", i18next.t("permission:Upcoming"), <ClockCircleOutlined />);
return Setting.getTag("warning", i18next.t("subscription:Upcoming"), <ClockCircleOutlined />);
case "Expired":
return Setting.getTag("warning", i18next.t("permission:Expired"), <ClockCircleOutlined />);
return Setting.getTag("warning", i18next.t("subscription:Expired"), <ClockCircleOutlined />);
case "Error":
return Setting.getTag("error", i18next.t("permission:Error"), <CloseCircleOutlined />);
return Setting.getTag("error", i18next.t("subscription:Error"), <CloseCircleOutlined />);
case "Suspended":
return Setting.getTag("default", i18next.t("permission:Suspended"), <MinusCircleOutlined />);
return Setting.getTag("default", i18next.t("subscription:Suspended"), <MinusCircleOutlined />);
default:
return null;
}

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