@@ -81,6 +78,7 @@
---
+
## Why Casdoor
Casdoor is a **UI-first** identity provider and access management platform: one place to manage users, organizations, applications, and providers, with a modern web console. Authorization policies can be expressed with **[Casbin](https://casbin.org/)** (ACL, RBAC, ABAC, and more). Unlike reverse-proxy-centric auth companions, Casdoor is a dedicated auth server with broad protocol support, designed to be straightforward to self-host and integrate—see **[casdoor.ai](https://casdoor.ai)** for documentation.
@@ -88,18 +86,20 @@ Casdoor is a **UI-first** identity provider and access management platform: one
---
+
## 🌐 Live demos
-| Environment | URL | Description |
-|-------------|-----|-------------|
+| Environment | URL | Description |
+| ------------- | -------------------------------------------- | --------------------------------------------------------------------------- |
| **Read-only** | [door.casdoor.com](https://door.casdoor.com) | Global demo; **any modification or write operation will fail** (read-only). |
-| **Writable** | [demo.casdoor.com](https://demo.casdoor.com) | Full access for testing; **data is reset about every 5 minutes**. |
+| **Writable** | [demo.casdoor.com](https://demo.casdoor.com) | Full access for testing; **data is reset about every 5 minutes**. |
Default demo admin login (where applicable): `admin` / `123` — use only for demos; change credentials on your own deployment.
---
+
## 🚀 Quick start
Pick one deployment method below. To keep behavior consistent with upstream, the steps are aligned with official docs.
@@ -164,6 +164,7 @@ Official guide: [Try with Helm](https://casdoor.ai/docs/basic/try-with-helm)
---
+
## ✨ Features
@@ -222,6 +223,7 @@ Official guide: [Try with Helm](https://casdoor.ai/docs/basic/try-with-helm)
---
+
## Technology stack
Casdoor is built as a **frontend–backend separated** project:
@@ -234,6 +236,7 @@ Casdoor is built as a **frontend–backend separated** project:
---
+
## 📖 Documentation
**All product documentation, installation, and tutorials live at [casdoor.ai/docs/overview](https://casdoor.ai/docs/overview).** Start here, then use the sections below.
@@ -256,6 +259,7 @@ Casdoor is built as a **frontend–backend separated** project:
---
+
## 🔌 Integrations
Casdoor integrates with common languages and frameworks:
@@ -275,6 +279,7 @@ Browse the full list: [Integrations](https://casdoor.ai/docs/category/integratio
---
+
## 🤝 Community and support
- **Discord**: [Join our community](https://discord.gg/5rPsrAzK7S)
@@ -285,6 +290,7 @@ Browse the full list: [Integrations](https://casdoor.ai/docs/category/integratio
---
+
## 🌍 Contributing
If you have questions about Casdoor, you can **[open an issue](https://github.com/casdoor/casdoor/issues)**. Pull requests are welcome; **we recommend opening an issue first** so you can align with maintainers and the community before larger changes.
@@ -293,12 +299,13 @@ Please also read our [contribution guidelines](https://casdoor.ai/docs/contribut
### Translation and i18n
-- **Crowdin** is used for translation workflows: [casdoor-site on Crowdin](https://crowdin.com/project/casdoor-site).
+- **Crowdin** is used for translation workflows: [hcp-casdoor on Crowdin](https://crowdin.com/project/hcp-casdoor).
- The web app uses **i18next**. When you add or change user-visible strings under [`web/`](https://github.com/casdoor/casdoor/tree/master/web), update the English catalog at [`web/src/locales/en/data.json`](web/src/locales/en/data.json) accordingly.
---
+
## 📄 License
Casdoor is licensed under the [Apache License 2.0](https://github.com/casdoor/casdoor/blob/master/LICENSE).
diff --git a/crowdin.yml b/crowdin.yml
index dbcc79710af9..fc070065cccb 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -1,4 +1,4 @@
-project_id: '491513'
+project_id_env: 'CROWDIN_PROJECT_ID'
api_token_env: 'CROWDIN_PERSONAL_TOKEN'
preserve_hierarchy: true
files: [
diff --git a/docs/docker-build.md b/docs/docker-build.md
new file mode 100644
index 000000000000..6a7f6fc8ac80
--- /dev/null
+++ b/docs/docker-build.md
@@ -0,0 +1,141 @@
+# Docker build guide — HCP Casdoor
+
+This document describes how to build the project's Docker images locally (both the standard runtime image and the "all-in-one" image), how to run them for local testing, and how to push them to a registry (ECR) if desired.
+
+Files of interest
+
+- Dockerfile: [Dockerfile](Dockerfile)
+- CI build workflow: [.github/workflows/build.yml](.github/workflows/build.yml)
+
+Overview
+
+- The repository Dockerfile is a multi-stage build that defines these notable stages:
+ - `FRONT` — builds the frontend under `/web` (Node/Yarn).
+ - `BACK` — builds the Go backend and runs `./build.sh` to produce server artifacts.
+ - `STANDARD` — minimal Alpine runtime image containing the server binary + web build.
+ - `ALLINONE` — Debian-based image that bundles server, web build and entrypoint for quick trials.
+
+Prerequisites
+
+- Docker Engine (Docker Desktop on macOS/Windows or Docker on Linux).
+- Docker Buildx (modern Docker includes buildx; Docker Desktop exposes it).
+- For pushing to ECR: the AWS CLI configured locally with credentials that have ECR permissions, or other valid login method.
+
+Quick local setup (one-time)
+
+1. Create/use a buildx builder (recommended):
+
+```bash
+docker buildx create --name mybuilder --use || docker buildx use mybuilder
+docker buildx inspect --bootstrap
+```
+
+2. (Optional) Ensure BuildKit is enabled if you rely on it:
+
+```bash
+export DOCKER_BUILDKIT=1
+```
+
+Build the ALL-IN-ONE image (single-platform, load into local daemon)
+
+This is the same `ALLINONE` target used in CI.
+
+```bash
+docker buildx build --platform linux/amd64 --target ALLINONE --load \
+ --pull --progress=plain \
+ -t casdoor-all-in-one:local .
+```
+
+Notes:
+
+- `--load` places the resulting image into your local Docker daemon (works for a single platform only).
+- `--pull` refreshes base images before building.
+- `--progress=plain` gives more readable logs for debugging build failures.
+
+Fallback (simpler) build (non-buildx)
+
+If you're running on a Linux host that matches the target platform, you can use the classic build command:
+
+```bash
+docker build --target ALLINONE -t casdoor-all-in-one:local .
+```
+
+Build the STANDARD runtime image (single-platform)
+
+```bash
+docker buildx build --platform linux/amd64 --target STANDARD --load -t casdoor:local .
+```
+
+Run the image locally
+
+```bash
+docker run --rm -it -p 8000:8000 casdoor-all-in-one:local
+```
+
+The `ALLINONE` image's `ENTRYPOINT`/`CMD` execute `/docker-entrypoint.sh` which starts the server (the image exposes the service on port 8000 by default). Use `docker logs ` if it exits immediately to see the startup errors.
+
+Mount local config or persist logs
+
+To substitute the configuration file or persist logs, mount host paths into the container:
+
+```bash
+docker run --rm -it -p 8000:8000 \
+ -v "$PWD/conf/app.conf":/conf/app.conf \
+ -v "$PWD/logs":/logs \
+ casdoor-all-in-one:local
+```
+
+Build multi-architecture images and push to a registry
+
+To build multi-arch and push to a remote registry (replace `` with your target):
+
+```bash
+docker buildx build --platform linux/amd64,linux/arm64 --target ALLINONE \
+ --push -t /casdoor-all-in-one:latest .
+```
+
+Pushing to AWS ECR (example)
+
+Local push to Amazon ECR requires valid AWS credentials locally. Example (replace `` and region if needed):
+
+```bash
+# login
+aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin .dkr.ecr.us-east-2.amazonaws.com
+
+# tag and push (single-platform)
+docker tag casdoor-all-in-one:local .dkr.ecr.us-east-2.amazonaws.com/casdoor-all-in-one:latest
+docker push .dkr.ecr.us-east-2.amazonaws.com/casdoor-all-in-one:latest
+
+# or buildx direct push for multi-arch
+docker buildx build --platform linux/amd64,linux/arm64 --target ALLINONE \
+ --push -t .dkr.ecr.us-east-2.amazonaws.com/casdoor-all-in-one:latest .
+```
+
+Note: In CI the repository uses Doormat to obtain AWS credentials and then `docker/build-push-action` to publish images. Locally you will either need `aws configure` credentials or another method to authenticate to ECR.
+
+Verification and debug commands
+
+```bash
+docker images | grep casdoor
+docker ps -a
+docker logs
+docker inspect
+```
+
+Common problems & tips
+
+- Build is slow: enable caching or keep a persistent buildx builder (`docker buildx create --use`) to reuse layers.
+- Platform mismatch: use `--platform linux/amd64` for x86_64 builds on macOS/Apple Silicon (via emulation) or build native arm64 if you need it.
+- Entrypoint/script errors: inspect `/docker-entrypoint.sh` inside the image to verify permissions and content.
+- Not enough disk space: remove dangling images `docker system prune -af` carefully.
+
+Why the Dockerfile works as-is
+
+- The `BACK` stage runs `./build.sh` during the build to compile the server. That means you do not need to pre-build the binary on the host; the container build runs the compile steps inside the builder stage.
+
+See also
+
+- The project Dockerfile: [Dockerfile](Dockerfile)
+- The CI build & release flow that uses the same targets: [.github/workflows/build.yml](.github/workflows/build.yml)
+
+If you want, I can run a local build on your machine now (it will use your Docker daemon). Say "yes" and I will proceed, or tell me which image/target and platform you want built.
diff --git a/docs/hcp-casdoor-setup.md b/docs/hcp-casdoor-setup.md
new file mode 100644
index 000000000000..1efbd6ad9211
--- /dev/null
+++ b/docs/hcp-casdoor-setup.md
@@ -0,0 +1,266 @@
+# HCP Casdoor Setup Guide
+
+This guide walks through the full setup for the forked repository:
+
+- Crowdin translation sync
+- AWS ECR bootstrap in integration and production
+- Manual build and release publishing to ECR
+
+It assumes the repository is `hashicorp-forge/casdoor`, the Crowdin project is `hcp-casdoor`, and the AWS region is `us-east-2`.
+
+## Quick Reference
+
+| Item | Value |
+| -------------------- | --------------------------------------------------------- |
+| Crowdin project name | `hcp-casdoor` |
+| Crowdin project ID | `892410` |
+| Crowdin project URL | `https://crowdin.com/project/hcp-casdoor` |
+| Crowdin project type | File-based |
+| Crowdin visibility | Private |
+| Source language | English |
+| AWS region | `us-east-2` |
+| Integration account | `995429640676` |
+| Integration role ARN | `arn:aws:iam::995429640676:role/DoormatGithubActionsRole` |
+| Production account | `877995958936` |
+| Production role ARN | `arn:aws:iam::877995958936:role/DoormatAdminRole` |
+| ECR repositories | `casdoor`, `casdoor-all-in-one` |
+
+## What Is Already Configured
+
+The repository already contains these workflows and files:
+
+- `.github/workflows/sync.yml` for Crowdin sync
+- `.github/workflows/bootstrap-ecr.yml` for ECR repository bootstrap
+- `.github/workflows/build.yml` for manual build and release publishing
+- `crowdin.yml` for backend translation files
+- `web/crowdin.yml` for frontend translation files
+
+The Crowdin configs read the project ID from `CROWDIN_PROJECT_ID`, so the GitHub workflow supplies the value instead of hardcoding it in two places.
+
+## Prerequisites
+
+Before you run anything, make sure you have:
+
+1. Admin or maintainer access to the GitHub repository.
+2. A Crowdin account and a private file-based project named `hcp-casdoor`.
+3. Access to create and manage GitHub repository secrets.
+4. Access to run GitHub Actions workflows in the repository.
+
+You do not need AWS access keys in GitHub secrets. The workflows use Doormat to obtain AWS credentials at runtime.
+
+## 1. Create or Verify the Crowdin Project
+
+1. Log in to Crowdin and open the project `hcp-casdoor`.
+2. Make the project private.
+3. Choose a file-based project.
+4. Set the source language to English.
+5. Select the initial target languages you want to support.
+6. Skip the onboarding app unless you want the extra guided setup experience.
+7. Confirm the project ID is `892410`.
+8. Confirm the project URL is `https://crowdin.com/project/hcp-casdoor`.
+
+Notes:
+
+- Selecting the top 30 languages is fine to start.
+- You can add more target languages later.
+- One shared Crowdin project is the correct choice here because both the frontend and backend translation files point to the same project ID.
+
+## 2. Add the Required GitHub Secret
+
+Add this repository secret in GitHub:
+
+- `CROWDIN_PERSONAL_TOKEN`
+
+This token lets the Crowdin GitHub Action talk to Crowdin. Keep it in GitHub Secrets only. Do not store it in the repository.
+
+You do not need to add these as repository secrets:
+
+- `CROWDIN_PROJECT_ID` because the workflow sets it to `892410`
+- `GITHUB_TOKEN` because GitHub provides it automatically to workflows
+- AWS access keys because Doormat handles AWS authentication
+
+## 3. Set the GitHub Actions Permissions
+
+The Crowdin workflow creates pull requests automatically. To allow that, check the repository settings:
+
+1. Open the repository settings.
+2. Go to the Actions settings page.
+3. Make sure workflow permissions allow write access.
+4. Allow GitHub Actions to create and approve pull requests if your repository policy requires it.
+
+The Crowdin workflow uses the built-in `GITHUB_TOKEN`, so no personal access token is needed for GitHub itself in the current setup.
+
+## 4. Run the Crowdin Sync Workflow
+
+The Crowdin workflow is manual now.
+
+1. Open the repository Actions tab.
+2. Select `Crowdin Action`.
+3. Run the workflow with `workflow_dispatch`.
+4. Confirm that the workflow uses the repository secret `CROWDIN_PERSONAL_TOKEN`.
+5. Confirm that it creates or updates a pull request into `master`.
+
+What the workflow does:
+
+- Uploads the backend source files from `crowdin.yml`.
+- Uploads the frontend source files from `web/crowdin.yml`.
+- Downloads translations.
+- Pushes the translation changes to the localization branch.
+- Opens a pull request automatically.
+
+If the workflow fails, check these items first:
+
+- The Crowdin project ID is still `892410`.
+- The `CROWDIN_PERSONAL_TOKEN` secret exists.
+- Repository Actions permissions allow pull request creation.
+- The project is private and file-based.
+
+## 5. Bootstrap ECR in Both AWS Accounts
+
+The ECR bootstrap workflow creates the repositories in the two AWS accounts if they do not already exist.
+
+1. Open the repository Actions tab.
+2. Select `Bootstrap ECR`.
+3. Run the workflow with `workflow_dispatch`.
+4. Keep the default role ARNs unless you intentionally changed them.
+5. Confirm the workflow runs in `us-east-2`.
+6. Review the job summary for the repository URIs and ARNs.
+
+The workflow creates these repositories in both accounts:
+
+- `casdoor`
+- `casdoor-all-in-one`
+
+The integration account and default role are:
+
+- Account: `995429640676`
+- Role ARN: `arn:aws:iam::995429640676:role/DoormatGithubActionsRole`
+
+The production account and default role are:
+
+- Account: `877995958936`
+- Role ARN: `arn:aws:iam::877995958936:role/DoormatAdminRole`
+
+No AWS secret is needed for this workflow. The workflow uses the pinned Doormat GitHub Action to assume the AWS role and export temporary credentials for the job.
+
+If you need to verify the result, open AWS and confirm that each account has:
+
+- `casdoor`
+- `casdoor-all-in-one`
+
+## 6. Run the Manual Build and Release Workflow
+
+The `Build` workflow is also manual.
+
+1. Open the repository Actions tab.
+2. Select `Build`.
+3. Run the workflow with `workflow_dispatch`.
+4. Leave the role ARN inputs at their defaults unless the AWS roles changed.
+5. Review the job output after the run finishes.
+
+The workflow does the following:
+
+- Runs tests and the backend/frontend build steps.
+- Runs semantic release.
+- Assumes the integration AWS role and production AWS role through Doormat.
+- Logs in to both ECR registries.
+- Pushes the standard image and the all-in-one image to both accounts.
+
+Important note:
+
+- The Docker publish steps only run when semantic release reports a new release.
+- If no new release is detected, the publish jobs skip.
+
+## 7. Understand the Crowdin Authentication Choice
+
+This repository uses the built-in `GITHUB_TOKEN` for Crowdin pull request creation.
+
+### Why this is the simplest option
+
+- It is created automatically by GitHub.
+- It does not require another long-lived secret.
+- It is scoped to the repository.
+- It is the lowest-friction setup for same-repo pull requests.
+
+### Tradeoffs
+
+- It only works inside the repository workflow context.
+- It depends on repository Actions permissions.
+- It is not ideal if you need a stable human identity on commits.
+
+### When to use other options later
+
+- Use a personal access token if you need a human identity or cross-repo access.
+- Use a GitHub App token if you want tighter least-privilege controls and are willing to spend more time on setup.
+
+### Current setup steps for `GITHUB_TOKEN`
+
+1. Keep `permissions: contents: write` and `pull-requests: write` in the Crowdin workflow.
+2. Leave `env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` in the workflow.
+3. Make sure repository settings allow GitHub Actions to create pull requests.
+
+## 8. Keep the Public Links Updated
+
+The repository now points at the new Crowdin project URL:
+
+- `https://crowdin.com/project/hcp-casdoor`
+
+The README no longer shows the Crowdin badge. The web app still links to the Crowdin project from the translation notice.
+
+If the Crowdin project ever changes again, update these locations:
+
+- README project link
+- Web app Crowdin link
+- Crowdin workflow project ID
+
+## 9. Final Setup Checklist
+
+Use this checklist to confirm everything is ready:
+
+- [ ] Crowdin project `hcp-casdoor` exists and is private
+- [ ] Crowdin project ID is `892410`
+- [ ] Crowdin is file-based, not string-based
+- [ ] `CROWDIN_PERSONAL_TOKEN` is stored as a GitHub secret
+- [ ] Repository Actions permissions allow PR creation
+- [ ] `Bootstrap ECR` has been run successfully
+- [ ] `Build` has been run successfully
+- [ ] README and the web app point to `hcp-casdoor` on Crowdin
+- [ ] No AWS access keys were added to GitHub secrets
+
+## 10. Troubleshooting
+
+### Crowdin PR is not created
+
+Check the following:
+
+- `CROWDIN_PERSONAL_TOKEN` exists and is correct.
+- GitHub Actions permissions allow write access.
+- GitHub Actions can create pull requests in the repository settings.
+- The workflow is running in the right repository.
+
+### Crowdin sync uses the wrong project
+
+Check the following:
+
+- `CROWDIN_PROJECT_ID` is still `892410` in the workflow.
+- `crowdin.yml` and `web/crowdin.yml` still use `project_id_env`.
+
+### ECR bootstrap fails
+
+Check the following:
+
+- The workflow inputs still point to the correct role ARNs.
+- The AWS role trust policy allows Doormat to assume the role.
+- The workflow is running in `us-east-2`.
+
+### Build publishes nothing
+
+Check the following:
+
+- Semantic release detected a new release.
+- The release job completed successfully before the Docker job.
+- The ECR bootstrap workflow already created the repositories.
+
+### You want to change the GitHub token approach later
+
+You can switch to a personal access token or GitHub App token later if the repository policy changes. For now, `GITHUB_TOKEN` is the easiest path and keeps the setup lightweight.
diff --git a/object/saml_idp.go b/object/saml_idp.go
index cd9ee28c44e0..48e4a33eab6c 100644
--- a/object/saml_idp.go
+++ b/object/saml_idp.go
@@ -162,6 +162,23 @@ func (x X509Key) GetKeyPair() (privateKey *rsa.PrivateKey, cert []byte, err erro
return privateKey, cert, err
}
+func newSamlSigningContext(application *Application, keyStore dsig.X509KeyStore) *dsig.SigningContext {
+ ctx := dsig.NewDefaultSigningContext(keyStore)
+ if application.SamlHashAlgorithm == "" || application.SamlHashAlgorithm == "SHA1" {
+ ctx.Hash = crypto.SHA1
+ } else if application.SamlHashAlgorithm == "SHA256" {
+ ctx.Hash = crypto.SHA256
+ } else if application.SamlHashAlgorithm == "SHA512" {
+ ctx.Hash = crypto.SHA512
+ }
+
+ if application.EnableSamlC14n10 {
+ ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("")
+ }
+
+ return ctx
+}
+
// IdpEntityDescriptor
// SAML METADATA
type IdpEntityDescriptor struct {
@@ -375,18 +392,7 @@ func GetSamlResponse(application *Application, user *User, samlRequest string, h
PrivateKey: cert.PrivateKey,
X509Certificate: certificate,
}
- ctx := dsig.NewDefaultSigningContext(randomKeyStore)
- if application.SamlHashAlgorithm == "" || application.SamlHashAlgorithm == "SHA1" {
- ctx.Hash = crypto.SHA1
- } else if application.SamlHashAlgorithm == "SHA256" {
- ctx.Hash = crypto.SHA256
- } else if application.SamlHashAlgorithm == "SHA512" {
- ctx.Hash = crypto.SHA512
- }
-
- if application.EnableSamlC14n10 {
- ctx.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList("xs")
- }
+ ctx := newSamlSigningContext(application, randomKeyStore)
// signedXML, err := ctx.SignEnvelopedLimix(samlResponse)
// if err != nil {
diff --git a/object/saml_idp_test.go b/object/saml_idp_test.go
new file mode 100644
index 000000000000..456c7d383ef1
--- /dev/null
+++ b/object/saml_idp_test.go
@@ -0,0 +1,140 @@
+// Copyright 2026 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 (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/pem"
+ "math/big"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/beevik/etree"
+)
+
+func TestNewSamlSigningContextCanonicalizer(t *testing.T) {
+ keyStore := newSamlSigningTestKeyStore(t)
+
+ tests := []struct {
+ name string
+ enableSamlC14n10 bool
+ wantAlgorithm string
+ unwantedAlgorithm string
+ wantPrefixListXs bool
+ }{
+ {
+ name: "default branch keeps c14n11",
+ enableSamlC14n10: false,
+ wantAlgorithm: "http://www.w3.org/2006/12/xml-c14n11",
+ unwantedAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
+ wantPrefixListXs: false,
+ },
+ {
+ name: "c14n10 branch uses exclusive canonicalization without prefix list",
+ enableSamlC14n10: true,
+ wantAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
+ unwantedAlgorithm: "http://www.w3.org/2006/12/xml-c14n11",
+ wantPrefixListXs: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := newSamlSigningContext(&Application{EnableSamlC14n10: tt.enableSamlC14n10}, keyStore)
+ signatureXML := buildSamlSignatureXML(t, ctx)
+
+ if !strings.Contains(signatureXML, tt.wantAlgorithm) {
+ t.Fatalf("signature = %q, want algorithm %q", signatureXML, tt.wantAlgorithm)
+ }
+
+ if tt.unwantedAlgorithm != "" && strings.Contains(signatureXML, tt.unwantedAlgorithm) {
+ t.Fatalf("signature = %q, found unexpected algorithm %q", signatureXML, tt.unwantedAlgorithm)
+ }
+
+ hasPrefixListXs := strings.Contains(signatureXML, `PrefixList="xs"`) || strings.Contains(signatureXML, "InclusiveNamespaces")
+ if hasPrefixListXs != tt.wantPrefixListXs {
+ t.Fatalf("signature = %q, PrefixList xs presence = %v, want %v", signatureXML, hasPrefixListXs, tt.wantPrefixListXs)
+ }
+ })
+ }
+}
+
+func buildSamlSignatureXML(t *testing.T, ctx interface {
+ ConstructSignature(el *etree.Element, enveloped bool) (*etree.Element, error)
+}) string {
+ t.Helper()
+
+ assertion := etree.NewElement("saml:Assertion")
+ assertion.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
+ assertion.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
+ assertion.CreateAttr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
+ assertion.CreateAttr("ID", "_test-assertion")
+ assertion.CreateElement("saml:Issuer").SetText("https://example.com")
+ attributeValue := assertion.CreateElement("saml:AttributeStatement").CreateElement("saml:Attribute").CreateElement("saml:AttributeValue")
+ attributeValue.CreateAttr("xsi:type", "xs:string")
+ attributeValue.SetText("user@example.com")
+
+ signature, err := ctx.ConstructSignature(assertion, true)
+ if err != nil {
+ t.Fatalf("ConstructSignature() error = %v", err)
+ }
+
+ doc := etree.NewDocument()
+ doc.SetRoot(signature)
+
+ bytes, err := doc.WriteToBytes()
+ if err != nil {
+ t.Fatalf("WriteToBytes() error = %v", err)
+ }
+
+ return string(bytes)
+}
+
+func newSamlSigningTestKeyStore(t *testing.T) *X509Key {
+ t.Helper()
+
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("GenerateKey() error = %v", err)
+ }
+
+ template := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "casdoor.test",
+ },
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ BasicConstraintsValid: true,
+ }
+
+ certificateDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
+ if err != nil {
+ t.Fatalf("CreateCertificate() error = %v", err)
+ }
+
+ privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
+
+ return &X509Key{
+ PrivateKey: string(privateKeyPEM),
+ X509Certificate: base64.StdEncoding.EncodeToString(certificateDER),
+ }
+}
diff --git a/web/crowdin.yml b/web/crowdin.yml
index 3929ddba3b9a..366ea1a7361b 100644
--- a/web/crowdin.yml
+++ b/web/crowdin.yml
@@ -1,4 +1,4 @@
-project_id: '491513'
+project_id_env: 'CROWDIN_PROJECT_ID'
api_token_env: 'CROWDIN_PERSONAL_TOKEN'
preserve_hierarchy: true
files: [
diff --git a/web/package.json b/web/package.json
index 5f1de31f48cc..ed3a9708a224 100644
--- a/web/package.json
+++ b/web/package.json
@@ -54,10 +54,10 @@
"react-highlight-words": "^0.18.0",
"react-i18next": "^11.8.7",
"react-metamask-avatar": "^1.2.1",
- "reactflow": "^11.11.4",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-social-login-buttons": "^3.4.0",
+ "reactflow": "^11.11.4",
"xlsx": "^0.18.5"
},
"scripts": {
diff --git a/web/src/App.js b/web/src/App.js
index 45d6f9d335b7..11596fbfd400 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -17,9 +17,25 @@ import "./App.less";
import {Helmet} from "react-helmet";
import * as Setting from "./Setting";
import {setOrgIsTourVisible, setTourLogo} from "./TourConfig";
-import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
-import {GithubOutlined, InfoCircleFilled, ShareAltOutlined} from "@ant-design/icons";
-import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tooltip} from "antd";
+import {
+ StyleProvider,
+ legacyLogicalPropertiesTransformer
+} from "@ant-design/cssinjs";
+import {
+ GithubOutlined,
+ InfoCircleFilled,
+ ShareAltOutlined
+} from "@ant-design/icons";
+import {
+ Alert,
+ Button,
+ ConfigProvider,
+ Drawer,
+ FloatButton,
+ Layout,
+ Result,
+ Tooltip
+} from "antd";
import {AiDots} from "./common/Loading";
import {Route, Switch, withRouter} from "react-router-dom";
import CustomGithubCorner from "./common/CustomGithubCorner";
@@ -72,33 +88,33 @@ setTwoToneColor("rgb(87,52,211)");
function getAntdLocale(language) {
const localeMap = {
- "en": enUS,
- "zh": zhCN,
+ en: enUS,
+ zh: zhCN,
"zh-tw": zhTW,
- "es": esES,
- "fr": frFR,
- "de": deDE,
- "id": idID,
- "ja": jaJP,
- "ko": koKR,
- "ru": ruRU,
- "vi": viVN,
- "pt": ptBR,
- "it": itIT,
- "ms": msMY,
- "tr": trTR,
- "ar": arEG,
- "he": heIL,
- "nl": nlNL,
- "pl": plPL,
- "fi": fiFI,
- "sv": svSE,
- "uk": ukUA,
- "fa": faIR,
- "cs": csCZ,
- "sk": skSK,
- "kk": ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh
- "az": trTR, // Use Turkish for Azerbaijani as they're similar
+ es: esES,
+ fr: frFR,
+ de: deDE,
+ id: idID,
+ ja: jaJP,
+ ko: koKR,
+ ru: ruRU,
+ vi: viVN,
+ pt: ptBR,
+ it: itIT,
+ ms: msMY,
+ tr: trTR,
+ ar: arEG,
+ he: heIL,
+ nl: nlNL,
+ pl: plPL,
+ fi: fiFI,
+ sv: svSE,
+ uk: ukUA,
+ fa: faIR,
+ cs: csCZ,
+ sk: skSK,
+ kk: ruRU, // Use Russian for Kazakh as antd doesn't have Kazakh
+ az: trTR, // Use Turkish for Azerbaijani as they're similar
};
return localeMap[language] || enUS;
}
@@ -109,7 +125,9 @@ class App extends Component {
this.setThemeAlgorithm();
let storageThemeAlgorithm = [];
try {
- storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") ? JSON.parse(localStorage.getItem("themeAlgorithm")) : ["default"];
+ storageThemeAlgorithm = localStorage.getItem("themeAlgorithm")
+ ? JSON.parse(localStorage.getItem("themeAlgorithm"))
+ : ["default"];
} catch {
storageThemeAlgorithm = ["default"];
}
@@ -147,16 +165,24 @@ class App extends Component {
}
if (this.state.account !== prevState.account) {
- const requiredEnableMfa = Setting.isRequiredEnableMfa(this.state.account, this.state.account?.organization);
+ const requiredEnableMfa = Setting.isRequiredEnableMfa(
+ this.state.account,
+ this.state.account?.organization
+ );
this.setState({
requiredEnableMfa: requiredEnableMfa,
});
if (requiredEnableMfa === true) {
- const mfaType = Setting.getMfaItemsByRules(this.state.account, this.state.account?.organization, [Setting.MfaRuleRequired])
- .find((item) => item.rule === Setting.MfaRuleRequired)?.name;
+ const mfaType = Setting.getMfaItemsByRules(
+ this.state.account,
+ this.state.account?.organization,
+ [Setting.MfaRuleRequired]
+ ).find((item) => item.rule === Setting.MfaRuleRequired)?.name;
if (mfaType !== undefined) {
- this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {from: "/login"});
+ this.props.history.push(`/mfa/setup?mfaType=${mfaType}`, {
+ from: "/login",
+ });
}
}
}
@@ -164,7 +190,9 @@ class App extends Component {
shouldFlattenMenu() {
const organization = this.state.account?.organization;
- const navItems = Setting.isLocalAdminUser(this.state.account) ? organization?.navItems : (organization?.userNavItems ?? []);
+ const navItems = Setting.isLocalAdminUser(this.state.account)
+ ? organization?.navItems
+ : (organization?.userNavItems ?? []);
// If navItems is "all" or not configured, don't flatten
if (!Array.isArray(navItems) || navItems?.includes("all")) {
@@ -174,17 +202,52 @@ class App extends Component {
// Count how many valid menu items would be visible
// Filter out any invalid or non-existent menu items
const validMenuItems = [
- "/", "/shortcuts", "/apps", // Home group
- "/organizations", "/groups", "/users", "/invitations", // User Management
- "/applications", "/providers", "/resources", "/certs", "/keys", // Identity
- "/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
- "/agents", "/servers", "/server-store", "/entries", "/sites", "/rules", // LLM AI
- "/sessions", "/records", "/tokens", "/verifications", // Auditing
- "/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
- "/sysinfo", "/forms", "/syncers", "/webhooks", "/webhook-events", "/tickets", "/swagger", // Admin
+ "/",
+ "/shortcuts",
+ "/apps", // Home group
+ "/organizations",
+ "/groups",
+ "/users",
+ "/invitations", // User Management
+ "/applications",
+ "/providers",
+ "/resources",
+ "/certs",
+ "/keys", // Identity
+ "/roles",
+ "/permissions",
+ "/models",
+ "/adapters",
+ "/enforcers", // Authorization
+ "/agents",
+ "/servers",
+ "/server-store",
+ "/entries",
+ "/sites",
+ "/rules", // LLM AI
+ "/sessions",
+ "/records",
+ "/tokens",
+ "/verifications", // Auditing
+ "/products",
+ "/orders",
+ "/payments",
+ "/plans",
+ "/pricings",
+ "/subscriptions",
+ "/transactions", // Business
+ "/sysinfo",
+ "/forms",
+ "/syncers",
+ "/webhooks",
+ "/webhook-events",
+ "/tickets",
+ "/swagger", // Admin
];
- const count = navItems.filter(item => validMenuItems.includes(item)).length;
+ const count = navItems.filter((item) =>
+ validMenuItems.includes(item)
+ ).length;
return count <= Conf.MaxItemsForFlatMenu;
}
@@ -198,7 +261,13 @@ class App extends Component {
} else if (uri.includes("/apps")) {
return "/apps";
}
- } else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
+ } else if (
+ uri.includes("/organizations") ||
+ uri.includes("/trees") ||
+ uri.includes("/groups") ||
+ uri.includes("/users") ||
+ uri.includes("/invitations")
+ ) {
if (uri.includes("/organizations")) {
return "/organizations";
} else if (uri.includes("/groups")) {
@@ -208,7 +277,12 @@ class App extends Component {
} else if (uri.includes("/invitations")) {
return "/invitations";
}
- } else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs")) {
+ } else if (
+ uri.includes("/applications") ||
+ uri.includes("/providers") ||
+ uri.includes("/resources") ||
+ uri.includes("/certs")
+ ) {
if (uri.includes("/applications")) {
return "/applications";
} else if (uri.includes("/providers")) {
@@ -220,7 +294,13 @@ class App extends Component {
}
} else if (uri.includes("/keys")) {
return "/keys";
- } else if (uri.includes("/agents") || uri.includes("/servers") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) {
+ } else if (
+ uri.includes("/agents") ||
+ uri.includes("/servers") ||
+ uri.includes("/entries") ||
+ uri.includes("/sites") ||
+ uri.includes("/rules")
+ ) {
if (uri.includes("/agents")) {
return "/agents";
} else if (uri.includes("/servers")) {
@@ -234,7 +314,13 @@ class App extends Component {
} else if (uri.includes("/rules")) {
return "/rules";
}
- } else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
+ } else if (
+ uri.includes("/roles") ||
+ uri.includes("/permissions") ||
+ uri.includes("/models") ||
+ uri.includes("/adapters") ||
+ uri.includes("/enforcers")
+ ) {
if (uri.includes("/roles")) {
return "/roles";
} else if (uri.includes("/permissions")) {
@@ -246,7 +332,12 @@ class App extends Component {
} else if (uri.includes("/enforcers")) {
return "/enforcers";
}
- } else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
+ } else if (
+ uri.includes("/records") ||
+ uri.includes("/tokens") ||
+ uri.includes("/sessions") ||
+ uri.includes("/verifications")
+ ) {
if (uri.includes("/sessions")) {
return "/sessions";
} else if (uri.includes("/records")) {
@@ -256,7 +347,15 @@ class App extends Component {
} else if (uri.includes("/verifications")) {
return "/verifications";
}
- } else if (uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
+ } else if (
+ uri.includes("/products") ||
+ uri.includes("/orders") ||
+ uri.includes("/payments") ||
+ uri.includes("/plans") ||
+ uri.includes("/pricings") ||
+ uri.includes("/subscriptions") ||
+ uri.includes("/transactions")
+ ) {
if (uri.includes("/products")) {
return "/products";
} else if (uri.includes("/orders")) {
@@ -272,7 +371,14 @@ class App extends Component {
} else if (uri.includes("/transactions")) {
return "/transactions";
}
- } else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/webhook-events") || uri.includes("/tickets")) {
+ } else if (
+ uri.includes("/sysinfo") ||
+ uri.includes("/forms") ||
+ uri.includes("/syncers") ||
+ uri.includes("/webhooks") ||
+ uri.includes("/webhook-events") ||
+ uri.includes("/tickets")
+ ) {
if (uri.includes("/sysinfo")) {
return "/sysinfo";
} else if (uri.includes("/forms")) {
@@ -312,19 +418,65 @@ class App extends Component {
// Original logic for grouped menu
if (uri === "/" || uri.includes("/shortcuts") || uri.includes("/apps")) {
this.setState({selectedMenuKey: "/home"});
- } else if (uri.includes("/organizations") || uri.includes("/trees") || uri.includes("/groups") || uri.includes("/users") || uri.includes("/invitations")) {
+ } 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") || uri.includes("/keys")) {
+ } else if (
+ uri.includes("/applications") ||
+ uri.includes("/providers") ||
+ uri.includes("/resources") ||
+ uri.includes("/certs") ||
+ uri.includes("/keys")
+ ) {
this.setState({selectedMenuKey: "/identity"});
- } else if (uri.includes("/agents") || uri.includes("/servers") || uri.includes("/server-store") || uri.includes("/entries") || uri.includes("/sites") || uri.includes("/rules")) {
+ } else if (
+ uri.includes("/agents") ||
+ uri.includes("/servers") ||
+ uri.includes("/server-store") ||
+ uri.includes("/entries") ||
+ uri.includes("/sites") ||
+ uri.includes("/rules")
+ ) {
this.setState({selectedMenuKey: "/gateway"});
- } else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
+ } else if (
+ uri.includes("/roles") ||
+ uri.includes("/permissions") ||
+ uri.includes("/models") ||
+ uri.includes("/adapters") ||
+ uri.includes("/enforcers")
+ ) {
this.setState({selectedMenuKey: "/auth"});
- } else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {
+ } else if (
+ uri.includes("/records") ||
+ uri.includes("/tokens") ||
+ uri.includes("/sessions") ||
+ uri.includes("/verifications")
+ ) {
this.setState({selectedMenuKey: "/logs"});
- } else if (uri.includes("/product-store") || uri.includes("/products") || uri.includes("/orders") || uri.includes("/payments") || uri.includes("/plans") || uri.includes("/pricings") || uri.includes("/subscriptions") || uri.includes("/transactions")) {
+ } else if (
+ uri.includes("/product-store") ||
+ uri.includes("/products") ||
+ uri.includes("/orders") ||
+ uri.includes("/payments") ||
+ uri.includes("/plans") ||
+ uri.includes("/pricings") ||
+ uri.includes("/subscriptions") ||
+ uri.includes("/transactions")
+ ) {
this.setState({selectedMenuKey: "/business"});
- } else if (uri.includes("/sysinfo") || uri.includes("/forms") || uri.includes("/syncers") || uri.includes("/webhooks") || uri.includes("/webhook-events") || uri.includes("/tickets")) {
+ } else if (
+ uri.includes("/sysinfo") ||
+ uri.includes("/forms") ||
+ uri.includes("/syncers") ||
+ uri.includes("/webhooks") ||
+ uri.includes("/webhook-events") ||
+ uri.includes("/tickets")
+ ) {
this.setState({selectedMenuKey: "/admin"});
} else if (uri.includes("/signup")) {
this.setState({selectedMenuKey: "/signup"});
@@ -394,7 +546,9 @@ class App extends Component {
if (localStorage.getItem("themeAlgorithm")) {
let storageThemeAlgorithm = [];
try {
- storageThemeAlgorithm = JSON.parse(localStorage.getItem("themeAlgorithm"));
+ storageThemeAlgorithm = JSON.parse(
+ localStorage.getItem("themeAlgorithm")
+ );
} catch {
storageThemeAlgorithm = ["default"];
}
@@ -416,17 +570,16 @@ class App extends Component {
if (!applicationName) {
return;
}
- ApplicationBackend.getApplication("admin", applicationName)
- .then((res) => {
- if (res.status === "error") {
- Setting.showMessage("error", res.msg);
- return;
- }
+ ApplicationBackend.getApplication("admin", applicationName).then((res) => {
+ if (res.status === "error") {
+ Setting.showMessage("error", res.msg);
+ return;
+ }
- this.setState({
- application: res.data,
- });
+ this.setState({
+ application: res.data,
});
+ });
}
getAccount() {
@@ -439,7 +592,9 @@ class App extends Component {
const query2 = this.getLanguageParam(params);
if (query2 !== "") {
- const url = window.location.toString().replace(new RegExp(`[?&]${query2}`), "");
+ const url = window.location
+ .toString()
+ .replace(new RegExp(`[?&]${query2}`), "");
window.history.replaceState({}, document.title, url);
}
@@ -457,32 +612,37 @@ class App extends Component {
window.history.replaceState({}, document.title, newUrl);
}
- AuthBackend.getAccount(query)
- .then((res) => {
- let account = null;
- let accessToken = null;
- if (res.status === "ok") {
- account = res.data;
- account.organization = res.data2;
- accessToken = res.data.accessToken;
-
- if (!localStorage.getItem("language")) {
- this.setLanguage(account);
- }
- this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm);
- setTourLogo(account.organization.logo);
- setOrgIsTourVisible(account.organization.enableTour);
- } else {
- if (res.data !== "Please login first") {
- Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
- }
+ AuthBackend.getAccount(query).then((res) => {
+ let account = null;
+ let accessToken = null;
+ if (res.status === "ok") {
+ account = res.data;
+ account.organization = res.data2;
+ accessToken = res.data.accessToken;
+
+ if (!localStorage.getItem("language")) {
+ this.setLanguage(account);
}
+ this.setTheme(
+ Setting.getThemeData(account.organization),
+ Conf.InitThemeAlgorithm
+ );
+ setTourLogo(account.organization.logo);
+ setOrgIsTourVisible(account.organization.enableTour);
+ } else {
+ if (res.data !== "Please login first") {
+ Setting.showMessage(
+ "error",
+ `${i18next.t("application:Failed to sign in")}: ${res.msg}`
+ );
+ }
+ }
- this.setState({
- account: account,
- accessToken: accessToken,
- });
+ this.setState({
+ account: account,
+ accessToken: accessToken,
});
+ });
}
onUpdateAccount(account) {
@@ -496,26 +656,45 @@ class App extends Component {
footerHtml = footerHtml ?? this.state.application?.footerHtml;
return (
- {!this.state.account ? null : }
- {!this.state.account ? null : }
-
+ )}
+ {!this.state.account ? null : (
+
+ )}
+
);
@@ -528,15 +707,41 @@ class App extends Component {
-
+
AI Assistant
-
-
+
+
-
-
+
+
}
@@ -550,19 +755,30 @@ class App extends Component {
}}
open={this.state.isAiAssistantOpen}
>
-
+
);
}
isDoorPages() {
- return this.isEntryPages() ||
+ return (
+ this.isEntryPages() ||
window.location.pathname.startsWith("/callback") ||
- window.location.pathname.startsWith("/telegram-login");
+ window.location.pathname.startsWith("/telegram-login")
+ );
}
isEntryPages() {
- return window.location.pathname.startsWith("/signup") ||
+ return (
+ window.location.pathname.startsWith("/signup") ||
window.location.pathname.startsWith("/login") ||
window.location.pathname.startsWith("/forget") ||
window.location.pathname.startsWith("/prompt") ||
@@ -572,7 +788,8 @@ class App extends Component {
window.location.pathname.startsWith("/buy-plan") ||
window.location.pathname.startsWith("/qrcode") ||
window.location.pathname.startsWith("/consent") ||
- window.location.pathname.startsWith("/captcha");
+ window.location.pathname.startsWith("/captcha")
+ );
}
onClick = ({key}) => {
@@ -600,13 +817,22 @@ class App extends Component {
let footerHtml = null;
if (this.state.organization === undefined) {
const curCookie = Cookie.parse(document.cookie);
- if (curCookie["organizationTheme"] && curCookie["organizationTheme"] !== "null") {
+ if (
+ curCookie["organizationTheme"] &&
+ curCookie["organizationTheme"] !== "null"
+ ) {
themeData = JSON.parse(curCookie["organizationTheme"]);
}
- if (curCookie["organizationLogo"] && curCookie["organizationLogo"] !== "") {
+ if (
+ curCookie["organizationLogo"] &&
+ curCookie["organizationLogo"] !== ""
+ ) {
logo = curCookie["organizationLogo"];
}
- if (curCookie["organizationFootHtml"] && curCookie["organizationFootHtml"] !== "") {
+ if (
+ curCookie["organizationFootHtml"] &&
+ curCookie["organizationFootHtml"] !== ""
+ ) {
footerHtml = curCookie["organizationFootHtml"];
}
}
@@ -623,41 +849,92 @@ class App extends Component {
},
components: shadcnThemeComponents,
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
- }}>
-
+ }}
+ >
+
- {
- this.isEntryPages() ?
- {
- this.setState({
- application: application,
- });
- }}
- onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
- onUpdateAccount={(account) => this.onUpdateAccount(account)}
- updataThemeData={this.setTheme}
- /> :
-
- {this.onLoginSuccess(redirectUrl);}} />} />
- {this.onLoginSuccess(redirectUrl);}} />} />
- } />
- } />} />
-
- }
+ {this.isEntryPages() ? (
+ {
+ this.setState({
+ application: application,
+ });
+ }}
+ onLoginSuccess={(redirectUrl) => {
+ this.onLoginSuccess(redirectUrl);
+ }}
+ onUpdateAccount={(account) => this.onUpdateAccount(account)}
+ updataThemeData={this.setTheme}
+ />
+ ) : (
+
+ (
+ {
+ this.onLoginSuccess(redirectUrl);
+ }}
+ />
+ )}
+ />
+ (
+ {
+ this.onLoginSuccess(redirectUrl);
+ }}
+ />
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+
+
+ }
+ />
+ )}
+ />
+
+ )}
- {
- this.renderFooter(logo, footerHtml)
- }
- {
- this.renderAiAssistant()
- }
+ {this.renderFooter(logo, footerHtml)}
+ {this.renderAiAssistant()}
@@ -698,7 +975,10 @@ class App extends Component {
themeAlgorithm: nextThemeAlgorithm,
logo: this.getLogo(nextThemeAlgorithm),
});
- localStorage.setItem("themeAlgorithm", JSON.stringify(nextThemeAlgorithm));
+ localStorage.setItem(
+ "themeAlgorithm",
+ JSON.stringify(nextThemeAlgorithm)
+ );
}}
setLogoutState={() => {
this.setState({
@@ -706,12 +986,8 @@ class App extends Component {
});
}}
/>
- {
- this.renderFooter()
- }
- {
- this.renderAiAssistant()
- }
+ {this.renderFooter()}
+ {this.renderAiAssistant()}
}
@@ -730,33 +1006,49 @@ class App extends Component {
}
return (
-
-
-
- {i18next.t("general:Found some texts still not translated? Please help us translate at")}
-
-
- Crowdin
-
- ! 🙏
-
- } />
+
+
+
+ {i18next.t(
+ "general:Found some texts still not translated? Please help us translate at"
+ )}
+
+
+ Crowdin
+
+ ! 🙏
+
+ }
+ />
);
}
render() {
return (
- {(this.state.account === undefined || this.state.account === null) ?
+ {this.state.account === undefined || this.state.account === null ? (
-
- :
+
+
+ ) : (
{this.state.account.organization?.displayName}
- }
+ )}
}}
@@ -769,11 +1061,13 @@ class App extends Component {
},
components: shadcnThemeComponents,
algorithm: Setting.getAlgorithm(this.state.themeAlgorithm),
- }}>
-
- {
- this.renderPage()
- }
+ }}
+ >
+
+ {this.renderPage()}