Skip to content

Conversation

shivam-jainn
Copy link

@shivam-jainn shivam-jainn commented Oct 10, 2025

Description

Added Gitea SSO

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • [ x] Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

Screenshot 2025-08-05 at 4 48 29 PM

Test Scenarios

Did manual testing by creating account on gitea.com . And logged in with their oauth support .

References

#7137

Summary by CodeRabbit

  • New Features

    • Added Gitea as an OAuth provider for both instance and space logins.
    • Admin: new Gitea configuration page with toggle, form (host, client ID/secret), validation, copyable service details, success toasts, and discard-changes protection.
    • Login UI now displays a Gitea sign-in option when enabled.
  • API

    • Introduced Gitea OAuth initiate and callback endpoints for global and space scopes.
  • Error Handling

    • Added specific error codes and messages for Gitea misconfiguration and provider issues.

@Copilot Copilot AI review requested due to automatic review settings October 10, 2025 08:00
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds Gitea SSO (Single Sign-On) authentication support to the application, implementing a new OAuth provider alongside existing providers like Google, GitHub, and GitLab. The implementation includes both frontend components for configuration and backend authentication handling.

Key changes:

  • Added Gitea OAuth provider with complete authentication flow
  • Extended type definitions and configuration management for Gitea authentication
  • Added admin UI components for Gitea SSO configuration
  • Integrated Gitea authentication options in the login interface

Reviewed Changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/types/src/instance/base.ts Added is_gitea_enabled flag to instance configuration interface
packages/types/src/instance/auth.ts Extended authentication types to include Gitea configuration keys
apps/space/core/components/account/auth-forms/auth-root.tsx Added Gitea login button and OAuth flow integration
apps/api/plane/license/management/commands/configure_instance.py Added Gitea configuration setup in instance management command
apps/api/plane/license/api/views/instance.py Extended instance API to expose Gitea configuration status
apps/api/plane/authentication/views/space/gitea.py Implemented Gitea OAuth endpoints for space authentication
apps/api/plane/authentication/views/app/gitea.py Implemented Gitea OAuth endpoints for app authentication
apps/api/plane/authentication/views/init.py Added Gitea view imports
apps/api/plane/authentication/urls.py Added Gitea OAuth URL patterns
apps/api/plane/authentication/provider/oauth/gitea.py Core Gitea OAuth provider implementation
apps/api/plane/authentication/adapter/oauth.py Extended OAuth adapter to handle Gitea provider
apps/api/plane/authentication/adapter/error.py Added Gitea-specific error codes
apps/admin/core/components/authentication/gitea-config.tsx Gitea configuration toggle component
apps/admin/ce/components/authentication/authentication-modes.tsx Added Gitea to authentication modes list
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx Gitea authentication configuration page
apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx Layout wrapper for Gitea configuration
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx Detailed Gitea configuration form component

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

import GithubDarkLogo from "/public/logos/github-dark.svg";
import GitlabLogo from "/public/logos/gitlab-logo.svg";
import GoogleLogo from "/public/logos/google-logo.svg";
import GiteaLogo from "/public/logos/gitea-logo.svg"
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at the end of import statement.

Suggested change
import GiteaLogo from "/public/logos/gitea-logo.svg"
import GiteaLogo from "/public/logos/gitea-logo.svg";

Copilot uses AI. Check for mistakes.

Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds end-to-end Gitea OAuth support: admin UI to enable/configure Gitea, types/config flags, space app login option, API provider with OAuth flows (initiate/callback) for app and space scopes, error codes, and URL wiring. Includes a dedicated config form, layout/page, and server-side instance configuration handling.

Changes

Cohort / File(s) Change summary
Admin UI — Gitea configuration screens
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx, .../gitea/page.tsx, .../gitea/layout.tsx
New layout, page, and form to view/update Gitea config (host, client id/secret), toggle enablement, handle unsaved changes, and show copyable details.
Admin UI — Authentication modes registry
apps/admin/ce/components/authentication/authentication-modes.tsx, apps/admin/core/components/authentication/gitea-config.tsx
Registers Gitea as an authentication method; adds a configuration component with toggle/edit or configure state and navigation to the Gitea config page.
Space App — Auth entry points
apps/space/core/components/account/auth-forms/auth-root.tsx
Adds Gitea to OAuth providers list, wiring click to API initiate endpoint; respects is_gitea_enabled flag.
API — OAuth provider implementation (Gitea)
apps/api/plane/authentication/provider/oauth/gitea.py, apps/api/plane/authentication/adapter/oauth.py, apps/api/plane/authentication/adapter/error.py
Introduces GiteaOAuthProvider with token/userinfo/email retrieval, config validation, error mapping; extends adapter with provider-specific error code. Adds new error codes.
API — Views and routing (Gitea)
apps/api/plane/authentication/urls.py, apps/api/plane/authentication/views/__init__.py, apps/api/plane/authentication/views/app/gitea.py, apps/api/plane/authentication/views/space/gitea.py
Adds initiate/callback endpoints for app and space scopes, session state handling, redirects, and login integration; wires new routes and public exports.
Instance config — Flags and initialization
apps/api/plane/license/api/views/instance.py, apps/api/plane/license/management/commands/configure_instance.py
Adds IS_GITEA_ENABLED flag and Gitea keys (host, client id/secret) to config retrieval, API response, and setup command with environment-driven initialization.
Types — Public interfaces
packages/types/src/instance/auth.ts, packages/types/src/instance/base.ts
Extends type unions with IS_GITEA_ENABLED and Gitea config keys; adds is_gitea_enabled to IInstanceConfig.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin
  participant AdminUI as Admin UI
  participant API as API (Instance Config)
  Admin->>AdminUI: Open Gitea auth page
  AdminUI->>API: GET instance configurations
  API-->>AdminUI: Config (IS_GITEA_ENABLED, GITEA_* values)
  Admin->>AdminUI: Edit + Save Gitea config / Toggle enable
  AdminUI->>API: PATCH updateInstanceConfigurations
  API-->>AdminUI: Updated config
  AdminUI-->>Admin: Toast: success
  note over AdminUI,API: IS_GITEA_ENABLED controls visibility in Space App
Loading
sequenceDiagram
  autonumber
  actor User
  participant SpaceApp as Space App
  participant AuthAPI as API /auth (Gitea)
  participant Gitea as Gitea Server
  User->>SpaceApp: Click "Continue with Gitea"
  SpaceApp->>AuthAPI: GET /auth/gitea (initiate) [+next_path]
  AuthAPI->>AuthAPI: Create state, persist session
  AuthAPI-->>User: 302 Redirect to Gitea authorize
  User->>Gitea: GET /login/oauth/authorize?state=...
  Gitea-->>User: 302 Redirect with code&state
  User->>AuthAPI: GET /auth/gitea/callback?code&state
  AuthAPI->>Gitea: POST /login/oauth/access_token (code)
  Gitea-->>AuthAPI: Token JSON
  AuthAPI->>Gitea: GET /api/v1/user (+ /emails if needed)
  Gitea-->>AuthAPI: User info (+ email)
  AuthAPI->>AuthAPI: Create/lookup user, login
  AuthAPI-->>User: 302 Redirect to next_path or default
  alt Error
    AuthAPI-->>User: 302 Redirect with error=GITEA_* code
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I toggled a switch with a hoppity cheer,
“Gitea is ready!” I thumped with my ear.
From burrow to cloud, tokens in tow,
I nibble on scopes while the redirects flow.
Secret seeds planted, carrots aligned—
OAuth harvest: securely signed. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title currently includes the branch name and ticket number, which is noisy, and uses a slash-delimited format rather than clearly summarizing the primary change as a concise sentence. It does mention Gitea SSO but isn’t phrased as a clear, standalone summary of what’s being added. Please rename the pull request to a concise, descriptive title such as “Add Gitea SSO integration” that clearly summarizes the primary change without including branch names or ticket numbers.
Description Check ⚠️ Warning The description loosely follows the template but lacks a detailed overview of the actual changes and their impact, as the “Description” section only states “Added Gitea SSO” without summarizing what components were updated or how the integration works. Please expand the “Description” section to provide a detailed summary of the key changes, such as which frontend components, backend endpoints, configuration keys, and workflows were added or modified to implement Gitea SSO.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/plane/license/management/commands/configure_instance.py (1)

230-351: Critical: IS_GITEA_ENABLED may never be created on existing instances (global exists() gate).

The check if not InstanceConfiguration.objects.filter(key__in=keys).exists(): short-circuits when any one key exists, so newly introduced flags like IS_GITEA_ENABLED won’t be created on upgraded instances. Use per-key existence checks.

Also recommend defaulting GITEA_HOST to https://gitea.com for consistency with the admin UI and documentation.

Apply this diff to switch to per-key creation and set a sane default for GITEA_HOST in the Gitea branch:

-        keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED", "IS_GITEA_ENABLED"]
-        if not InstanceConfiguration.objects.filter(key__in=keys).exists():
-            for key in keys:
+        keys = ["IS_GOOGLE_ENABLED", "IS_GITHUB_ENABLED", "IS_GITLAB_ENABLED", "IS_GITEA_ENABLED"]
+        for key in keys:
+            if InstanceConfiguration.objects.filter(key=key).exists():
+                self.stdout.write(self.style.WARNING(f"{key} configuration already exists"))
+                continue
             if key == "IS_GOOGLE_ENABLED":
                 GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET = get_configuration_value(
                     [
                         {
                             "key": "GOOGLE_CLIENT_ID",
                             "default": os.environ.get("GOOGLE_CLIENT_ID", ""),
                         },
                         {
                             "key": "GOOGLE_CLIENT_SECRET",
                             "default": os.environ.get("GOOGLE_CLIENT_SECRET", "0"),
                         },
                     ]
                 )
                 if bool(GOOGLE_CLIENT_ID) and bool(GOOGLE_CLIENT_SECRET):
                     value = "1"
                 else:
                     value = "0"
                 InstanceConfiguration.objects.create(
                     key=key,
                     value=value,
                     category="AUTHENTICATION",
                     is_encrypted=False,
                 )
                 self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable."))
-            if key == "IS_GITHUB_ENABLED":
+            elif key == "IS_GITHUB_ENABLED":
                 GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET = get_configuration_value(
                     [
                         {
                             "key": "GITHUB_CLIENT_ID",
                             "default": os.environ.get("GITHUB_CLIENT_ID", ""),
                         },
                         {
                             "key": "GITHUB_CLIENT_SECRET",
                             "default": os.environ.get("GITHUB_CLIENT_SECRET", "0"),
                         },
                     ]
                 )
                 if bool(GITHUB_CLIENT_ID) and bool(GITHUB_CLIENT_SECRET):
                     value = "1"
                 else:
                     value = "0"
                 InstanceConfiguration.objects.create(
-                    key="IS_GITHUB_ENABLED",
+                    key="IS_GITHUB_ENABLED",
                     value=value,
                     category="AUTHENTICATION",
                     is_encrypted=False,
                 )
                 self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable."))
-            if key == "IS_GITLAB_ENABLED":
+            elif key == "IS_GITLAB_ENABLED":
                 GITLAB_HOST, GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET = get_configuration_value(
                     [
                         {
                             "key": "GITLAB_HOST",
                             "default": os.environ.get("GITLAB_HOST", "https://gitlab.com"),
                         },
                         {
                             "key": "GITLAB_CLIENT_ID",
                             "default": os.environ.get("GITLAB_CLIENT_ID", ""),
                         },
                         {
                             "key": "GITLAB_CLIENT_SECRET",
                             "default": os.environ.get("GITLAB_CLIENT_SECRET", ""),
                         },
                     ]
                 )
                 if bool(GITLAB_HOST) and bool(GITLAB_CLIENT_ID) and bool(GITLAB_CLIENT_SECRET):
                     value = "1"
                 else:
                     value = "0"
                 InstanceConfiguration.objects.create(
-                    key="IS_GITLAB_ENABLED",
+                    key="IS_GITLAB_ENABLED",
                     value=value,
                     category="AUTHENTICATION",
                     is_encrypted=False,
                 )
                 self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable."))
-            if key == "IS_GITEA_ENABLED":
+            elif key == "IS_GITEA_ENABLED":
                 GITEA_HOST, GITEA_CLIENT_ID, GITEA_CLIENT_SECRET = (
                     get_configuration_value(
                         [
                             {
                                 "key": "GITEA_HOST",
-                                "default": os.environ.get(
-                                    "GITEA_HOST", ""
-                                ),
+                                "default": os.environ.get("GITEA_HOST", "https://gitea.com"),
                             },
                             {
                                 "key": "GITEA_CLIENT_ID",
                                 "default": os.environ.get("GITEA_CLIENT_ID", ""),
                             },
                             {
                                 "key": "GITEA_CLIENT_SECRET",
                                 "default": os.environ.get(
                                     "GITEA_CLIENT_SECRET", ""
                                 ),
                             },
                         ]
                     )
                 )
                 if (
                     bool(GITEA_HOST)
                     and bool(GITEA_CLIENT_ID)
                     and bool(GITEA_CLIENT_SECRET)
                 ):
                     value = "1"
                 else:
                     value = "0"
                 InstanceConfiguration.objects.create(
                     key="IS_GITEA_ENABLED",
                     value=value,
                     category="AUTHENTICATION",
                     is_encrypted=False,
                 )
                 self.stdout.write(
                     self.style.SUCCESS(
                         f"{key} loaded with value from environment variable."
                     )
                 )
-        else:
-            for key in keys:
-                self.stdout.write(self.style.WARNING(f"{key} configuration already exists"))
🧹 Nitpick comments (8)
apps/space/core/components/account/auth-forms/auth-root.tsx (1)

196-204: Consider adding a trailing comma for consistency.

The Gitea OAuth configuration object is correctly structured and follows the pattern of other OAuth providers. However, for consistency with the other OAuth entries (Google, GitHub, GitLab) and to follow modern JavaScript/TypeScript best practices, consider adding a trailing comma after the closing brace.

Apply this diff to add the trailing comma:

     {
       id: "gitea",
       text: `${content} with Gitea`,
       icon: <Image src={GiteaLogo} height={18} width={18} alt="Gitea Logo" />,
       onClick: () => {
         window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
       },
       enabled: config?.is_gitea_enabled
-    }
+    },
   ];
apps/api/plane/license/management/commands/configure_instance.py (1)

197-201: Set a sensible default for GITEA_HOST.

Default GITEA_HOST to https://gitea.com to match the admin UI and docs.

-            {
-                "key": "GITEA_HOST",
-                "value": os.environ.get("GITEA_HOST"),
-                "category": "GITEA",
-                "is_encrypted": False,
-            },
+            {
+                "key": "GITEA_HOST",
+                "value": os.environ.get("GITEA_HOST", "https://gitea.com"),
+                "category": "GITEA",
+                "is_encrypted": False,
+            },
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1)

139-142: Reset GITEA_HOST to default after save (team convention).

Reset host to https://gitea.com post-submit instead of keeping the submitted value.

Based on learnings

-        reset({
-          GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
-          GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,    
-        });
+        reset({
+          GITEA_HOST: "https://gitea.com",
+          GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
+          GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
+        });
apps/admin/core/components/authentication/gitea-config.tsx (1)

35-44: Simplify toggle logic; avoid parseInt on empty strings.

Direct string comparison is clearer and avoids NaN edge cases.

-          <ToggleSwitch
-            value={Boolean(parseInt(GiteaConfig))}
-            onChange={() => {
-              Boolean(parseInt(GiteaConfig)) === true
-                ? updateConfig("IS_GITEA_ENABLED", "0")
-                : updateConfig("IS_GITEA_ENABLED", "1");
-            }}
+          <ToggleSwitch
+            value={GiteaConfig === "1"}
+            onChange={() => updateConfig("IS_GITEA_ENABLED", GiteaConfig === "1" ? "0" : "1")}
             size="sm"
             disabled={disabled}
           />
apps/api/plane/authentication/views/app/gitea.py (1)

97-103: Clear OAuth session artifacts after successful auth.

Remove state/next_path/host from session to avoid reuse or leakage.

-            if next_path:
+            if next_path:
                 path = str(validate_next_path(next_path))
             else:
                 path = get_redirection_path(user=user)
-            # redirect to referer path
+            # Clear transient session data
+            request.session.pop("state", None)
+            request.session.pop("next_path", None)
+            request.session.pop("host", None)
+            # redirect to referer path
             url = f"{host}{path}"
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx (2)

52-59: Use finally to simplify submission state handling.

Avoid duplicated setIsSubmitting calls.

-    await updateConfigPromise
-      .then(() => {
-        setIsSubmitting(false);
-      })
-      .catch((err) => {
-        console.error(err);
-        setIsSubmitting(false);
-      });
+    await updateConfigPromise.finally(() => setIsSubmitting(false));

10-10: Remove unused imports/vars.

resolveGeneralTheme and resolvedTheme are unused.

Also applies to: 26-26

apps/api/plane/authentication/provider/oauth/gitea.py (1)

56-56: Build redirect_uri using Django’s absolute URI helper.

request.is_secure may be inaccurate behind proxies; build_absolute_uri respects SECURE_PROXY_SSL_HEADER and forwarded proto.

-        redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/"
+        redirect_uri = request.build_absolute_uri("/auth/gitea/callback/")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71ec9fa and ca6740d.

⛔ Files ignored due to path filters (2)
  • apps/admin/public/logos/gitea-logo.svg is excluded by !**/*.svg
  • apps/web/public/logos/gitea-logo.svg is excluded by !**/*.svg
📒 Files selected for processing (17)
  • apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1 hunks)
  • apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx (1 hunks)
  • apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx (1 hunks)
  • apps/admin/ce/components/authentication/authentication-modes.tsx (3 hunks)
  • apps/admin/core/components/authentication/gitea-config.tsx (1 hunks)
  • apps/api/plane/authentication/adapter/error.py (1 hunks)
  • apps/api/plane/authentication/adapter/oauth.py (1 hunks)
  • apps/api/plane/authentication/provider/oauth/gitea.py (1 hunks)
  • apps/api/plane/authentication/urls.py (2 hunks)
  • apps/api/plane/authentication/views/__init__.py (2 hunks)
  • apps/api/plane/authentication/views/app/gitea.py (1 hunks)
  • apps/api/plane/authentication/views/space/gitea.py (1 hunks)
  • apps/api/plane/license/api/views/instance.py (3 hunks)
  • apps/api/plane/license/management/commands/configure_instance.py (3 hunks)
  • apps/space/core/components/account/auth-forms/auth-root.tsx (3 hunks)
  • packages/types/src/instance/auth.ts (2 hunks)
  • packages/types/src/instance/base.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-06T09:01:17.915Z
Learnt from: shivam-jainn
PR: makeplane/plane#7545
File: apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx:139-143
Timestamp: 2025-08-06T09:01:17.915Z
Learning: In the Plane project's Gitea authentication form (apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx), the GITEA_HOST field should not be included in the form reset after submission. It should default back to "https://gitea.com" rather than maintaining the submitted value, as this provides a sensible default for most users.

Applied to files:

  • apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx
🧬 Code graph analysis (11)
apps/admin/core/components/authentication/gitea-config.tsx (4)
packages/types/src/instance/auth.ts (1)
  • TInstanceAuthenticationMethodKeys (10-17)
apps/space/core/hooks/store/use-instance.ts (1)
  • useInstance (7-11)
apps/admin/core/store/instance.store.ts (1)
  • formattedConfig (83-89)
packages/propel/src/button/helper.tsx (1)
  • getButtonStyling (109-120)
apps/api/plane/authentication/views/app/gitea.py (6)
apps/api/plane/authentication/provider/oauth/gitea.py (1)
  • GiteaOAuthProvider (17-158)
apps/api/plane/license/models/instance.py (1)
  • Instance (18-46)
apps/api/plane/authentication/adapter/error.py (2)
  • AuthenticationException (72-87)
  • get_error_dict (82-87)
apps/api/plane/utils/path_validator.py (1)
  • validate_next_path (65-94)
apps/api/plane/authentication/views/space/gitea.py (2)
  • get (22-53)
  • get (57-98)
apps/api/plane/authentication/adapter/oauth.py (2)
  • get_auth_url (56-57)
  • authenticate (65-68)
apps/api/plane/license/management/commands/configure_instance.py (2)
apps/api/plane/license/api/views/instance.py (1)
  • get (32-183)
apps/api/plane/license/models/instance.py (1)
  • InstanceConfiguration (68-79)
apps/api/plane/authentication/urls.py (2)
apps/api/plane/authentication/views/app/gitea.py (2)
  • GiteaCallbackEndpoint (60-109)
  • GiteaOauthInitiateEndpoint (22-57)
apps/api/plane/authentication/views/space/gitea.py (2)
  • GiteaCallbackSpaceEndpoint (56-98)
  • GiteaOauthInitiateSpaceEndpoint (21-53)
apps/api/plane/license/api/views/instance.py (2)
apps/api/plane/authentication/views/space/gitea.py (2)
  • get (22-53)
  • get (57-98)
apps/api/plane/authentication/views/app/gitea.py (2)
  • get (23-57)
  • get (61-109)
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (7)
packages/types/src/instance/base.ts (1)
  • IFormattedInstanceConfiguration (97-99)
packages/types/src/instance/auth.ts (1)
  • TInstanceGiteaAuthenticationConfigurationKeys (31-34)
apps/space/core/hooks/store/use-instance.ts (1)
  • useInstance (7-11)
apps/admin/core/components/common/controller-input.tsx (2)
  • TControllerInputFormField (22-30)
  • ControllerInput (32-83)
apps/admin/core/components/common/copy-field.tsx (2)
  • TCopyField (15-20)
  • CopyField (22-46)
apps/admin/core/components/common/confirm-discard-modal.tsx (1)
  • ConfirmDiscardModal (16-74)
packages/propel/src/button/helper.tsx (1)
  • getButtonStyling (109-120)
apps/api/plane/authentication/views/space/gitea.py (6)
apps/api/plane/authentication/provider/oauth/gitea.py (1)
  • GiteaOAuthProvider (17-158)
apps/api/plane/license/models/instance.py (1)
  • Instance (18-46)
apps/api/plane/authentication/adapter/error.py (2)
  • AuthenticationException (72-87)
  • get_error_dict (82-87)
apps/api/plane/utils/path_validator.py (1)
  • validate_next_path (65-94)
apps/api/plane/authentication/views/app/gitea.py (2)
  • get (23-57)
  • get (61-109)
apps/api/plane/authentication/adapter/oauth.py (2)
  • get_auth_url (56-57)
  • authenticate (65-68)
apps/api/plane/authentication/views/__init__.py (2)
apps/api/plane/authentication/views/app/gitea.py (2)
  • GiteaCallbackEndpoint (60-109)
  • GiteaOauthInitiateEndpoint (22-57)
apps/api/plane/authentication/views/space/gitea.py (2)
  • GiteaCallbackSpaceEndpoint (56-98)
  • GiteaOauthInitiateSpaceEndpoint (21-53)
apps/api/plane/authentication/provider/oauth/gitea.py (2)
apps/api/plane/authentication/adapter/oauth.py (4)
  • OauthAdapter (19-124)
  • get_user_token (70-78)
  • set_user_data (90-91)
  • get_user_response (80-88)
apps/api/plane/authentication/adapter/error.py (1)
  • AuthenticationException (72-87)
apps/admin/ce/components/authentication/authentication-modes.tsx (1)
apps/admin/core/components/authentication/gitea-config.tsx (1)
  • GiteaConfiguration (20-57)
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx (4)
apps/space/core/hooks/store/use-instance.ts (1)
  • useInstance (7-11)
apps/admin/core/store/instance.store.ts (1)
  • formattedConfig (83-89)
apps/admin/core/components/authentication/authentication-method-card.tsx (1)
  • AuthenticationMethodCard (17-56)
apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1)
  • InstanceGiteaConfigForm (26-205)
🔇 Additional comments (19)
apps/api/plane/authentication/adapter/oauth.py (1)

51-52: LGTM!

The Gitea error code handling follows the same pattern as other OAuth providers.

packages/types/src/instance/base.ts (1)

45-45: LGTM!

The is_gitea_enabled field follows the same pattern as other OAuth provider flags.

apps/api/plane/authentication/adapter/error.py (2)

41-41: LGTM!

The error code entries follow the established pattern and numbering scheme for OAuth providers.


45-45: LGTM!

The error code is consistent with other OAuth provider error codes.

apps/admin/ce/components/authentication/authentication-modes.tsx (3)

18-18: LGTM!

The GiteaConfiguration import is correctly added alongside other OAuth provider configurations.


27-27: LGTM!

The Gitea logo import follows the same pattern as other OAuth provider logos.


85-91: LGTM!

The Gitea authentication mode configuration is consistent with other OAuth providers and includes all required fields.

apps/api/plane/license/api/views/instance.py (3)

53-53: LGTM!

The IS_GITEA_ENABLED configuration value is correctly added to the tuple following the pattern of other OAuth providers.


90-93: LGTM!

The configuration entry correctly retrieves the Gitea enabled flag with appropriate defaults.


142-142: LGTM!

The boolean conversion follows the same pattern as other OAuth provider flags.

apps/api/plane/authentication/views/__init__.py (2)

8-8: LGTM!

The Gitea endpoint imports follow the established pattern for OAuth providers.


21-22: LGTM!

The space-scoped Gitea endpoint imports are correctly added following the same pattern as other OAuth providers.

apps/space/core/components/account/auth-forms/auth-root.tsx (2)

30-30: LGTM!

The Gitea logo import follows the same pattern as other OAuth provider logos.


99-99: LGTM!

The isOAuthEnabled check correctly includes the Gitea configuration flag.

apps/admin/app/(all)/(dashboard)/authentication/gitea/layout.tsx (1)

1-10: LGTM!

The layout file follows Next.js App Router conventions and provides appropriate metadata for the Gitea authentication section.

apps/api/plane/authentication/urls.py (1)

136-148: Gitea routes wired correctly.

Endpoints and names mirror existing providers; import list updated accordingly. LGTM.

Also applies to: 39-42

apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx (1)

106-127: Confirm whether one or two Callback URIs are required.

The UI surfaces only /auth/gitea/callback/. We also have /spaces/gitea/callback/ routes. If Gitea requires both to be whitelisted, the form should display both URIs (or document the need to create two OAuth apps).

Would you confirm Gitea’s redirect URI policy? If multiple URIs are supported, I can add a second CopyField for the Space callback; otherwise, we should align flows to a single callback path.

packages/types/src/instance/auth.ts (1)

31-41: Types updated consistently for Gitea.

New method and configuration keys are correctly added and unioned. Looks good.

Also applies to: 10-18

apps/api/plane/authentication/views/app/gitea.py (1)

30-37: Consider gating by IS_GITEA_ENABLED.

Only proceed if Gitea is enabled in instance config, not just setup_done. Otherwise, backend still allows login when disabled in admin UI.

Do we already enforce IS_GITEA_ENABLED server-side elsewhere? If not, add a check here similar to provider-not-configured handling. I can draft it if needed.

Comment on lines +40 to +45
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value ? "active" : "disabled"}.`,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix toast message: string truthiness bug.

"0" is truthy, so message always says “active”. Compare explicitly.

-        message: () => `Gitea authentication is now ${value ? "active" : "disabled"}.`,
+        message: () => `Gitea authentication is now ${value === "1" ? "enabled" : "disabled"}.`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value ? "active" : "disabled"}.`,
},
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value === "1" ? "enabled" : "disabled"}.`,
},
🤖 Prompt for AI Agents
apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx around lines
40-45: the toast message uses the truthiness of value so a string "0" is treated
as true and always yields "active"; change the conditional to an explicit
comparison against the active indicator (for example compare value strictly to
the expected "active" token such as "1" or to boolean true depending on the prop
type), ensuring you normalize the type if needed, and use that explicit result
to choose between "active" and "disabled".

Comment on lines +51 to +63
self.scope = "user:email"

client_id = GITEA_CLIENT_ID
client_secret = GITEA_CLIENT_SECRET

redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/"
url_params = {
"client_id": client_id,
"scope": self.scope,
"redirect_uri": redirect_uri,
"response_type": "code",
"state": state,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Confirm Gitea scope value.

Scope is set to "user:email". Please verify it matches Gitea’s OAuth scopes for email access.


🌐 Web query:

What OAuth scopes does Gitea support for retrieving the authenticated user's primary email? Is "user:email" correct?

💡 Result:

Short answer: No — "user:email" is not the Gitea scope. Use OpenID/OIDC scopes.

Details:

  • For OIDC sign‑in and to receive the authenticated user's primary email via the UserInfo/jwt, request the OpenID scopes (openid plus email and/or profile/userinfo). The docs state Gitea exposes email/username via the OIDC scopes such as email and userinfo. [1]
  • To programmatically list a user's email addresses you can call GET /user/emails; access to that API is governed by Gitea's token scopes (granular scopes mapped to API routes — read vs write for resources). Gitea does not document a "user:email" GitHub‑style scope. Check the OAuth2 provider and API docs for the exact scope mapping on your Gitea version. [1][2]

Sources:
[1] Gitea OAuth2 / OpenID Connect provider documentation (scopes / OIDC notes).
[2] Gitea API reference showing GET /user/emails (list authenticated user's email addresses).


Use correct Gitea OIDC scopes for email retrieval.
Replace self.scope = "user:email" with the OpenID Connect scopes required by Gitea, for example:

self.scope = "openid email"
🤖 Prompt for AI Agents
In apps/api/plane/authentication/provider/oauth/gitea.py around lines 51 to 63,
the Gitea scope is set to "user:email" which is incorrect for OpenID Connect;
replace self.scope = "user:email" with the OIDC scopes (for example self.scope =
"openid email") ensuring scopes are space-separated and include "openid" and
"email" so Gitea returns an ID token and email claims during OIDC flows.

Comment on lines +110 to +128
def __get_email(self, headers):
try:
# Gitea may not provide email in user response, so fetch it separately
emails_url = f"{self.userinfo_url}/emails"
response = requests.get(emails_url, headers=headers)
if not response.ok:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
error_message=f"GITEA_OAUTH_PROVIDER_ERROR: Failed to fetch emails (status: {response.status_code}, response: {response.text})",
)
emails_response = response.json()
email = next(
(email["email"] for email in emails_response if email.get("primary")), None
)
if not email and emails_response:
# If no primary email, use the first one
email = emails_response[0].get("email")
return email
except requests.RequestException as e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeouts to external HTTP calls to avoid hanging request threads.

requests.get/post without a timeout can block indefinitely on network issues. Prefer adapter-level defaults.

Suggest updating OauthAdapter (apps/api/plane/authentication/adapter/oauth.py) to include timeouts:

# apps/api/plane/authentication/adapter/oauth.py
def get_user_token(self, data, headers=None):
    try:
        headers = headers or {}
        response = requests.post(self.get_token_url(), data=data, headers=headers, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.RequestException:
        ...

def get_user_response(self):
    try:
        headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"}
        response = requests.get(self.get_user_info_url(), headers=headers, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.RequestException:
        ...

If changing the adapter isn’t feasible right now, add timeout=10 to the provider’s requests.get calls here as an interim measure.

Also applies to: 135-145

@@ -0,0 +1,109 @@
import uuid
from urllib.parse import urlencode, urljoin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix redirect construction and session host fallback; validate next_path consistently.

  • urljoin with a leading "?" can drop the path component from base_host, producing wrong redirects.
  • Relying on session["host"] can be None (new session, expiry), causing TypeError in urljoin. Compute a safe fallback from the request.
  • Validate next_path before echoing it in error params (defense-in-depth).
  • Prefer simple string concat for base + path/query to preserve base_host’s path.

Apply this diff:

-from urllib.parse import urlencode, urljoin
+from urllib.parse import urlencode
@@
-            url = urljoin(
-                base_host(request=request, is_app=True), "?" + urlencode(params)
-            )
+            url = f"{base_host(request=request, is_app=True)}?{urlencode(params)}"
@@
-            url = urljoin(
-                base_host(request=request, is_app=True), "?" + urlencode(params)
-            )
+            url = f"{base_host(request=request, is_app=True)}?{urlencode(params)}"
@@
-        base_host = request.session.get("host")
+        host = request.session.get("host") or base_host(request=request, is_app=True)
@@
-            if next_path:
-                params["next_path"] = str(next_path)
-            url = urljoin(base_host, "?" + urlencode(params))
+            if next_path:
+                params["next_path"] = str(validate_next_path(next_path))
+            url = f"{host}?{urlencode(params)}"
@@
-            url = urljoin(base_host, "?" + urlencode(params))
+            url = f"{host}?{urlencode(params)}"
@@
-            # redirect to referer path
-            url = urljoin(base_host, path)
+            # redirect to referer path
+            url = f"{host}{path}"

Also applies to: 40-43, 54-57, 64-66, 72-76, 102-103

🤖 Prompt for AI Agents
In apps/api/plane/authentication/views/app/gitea.py around lines 2 and the other
affected ranges (40-43, 54-57, 64-66, 72-76, 102-103), fixes are needed for
constructing redirect URLs and handling session host/next_path: stop using
urljoin for appending a query or a leading-? (it can drop base path) and instead
build redirects with simple string concatenation (e.g., base_host.rstrip('/') +
path or base_host.rstrip('/') + '?' + urlencode(params)); avoid using
session["host"] directly since it can be None — compute a safe fallback like
host = session.get("host") or request.host_url.rstrip('/') (or request.scheme +
"://" + request.host if host_url is unavailable) and use that host; validate
next_path before echoing it in any error query parameter (e.g., ensure it is a
safe internal path: startswith('/') and contains no scheme/host or suspicious
characters) and only include it when valid; replace each urljoin usage and
direct session["host"] access in the listed line ranges with these safe
concatenation, fallback host computation, and next_path validation steps.

request.session["host"] = base_host(request=request, is_space=True)
next_path = request.GET.get("next_path")
if next_path:
request.session["next_path"] = str(next_path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Sanitize next_path to prevent open-redirects and unsafe output.

  • Store a validated next_path in session.
  • Validate next_path before echoing in error params and before final redirect.
-        if next_path:
-            request.session["next_path"] = str(next_path)
+        if next_path:
+            request.session["next_path"] = str(validate_next_path(next_path))
@@
-            if next_path:
-                params["next_path"] = str(next_path)
+            if next_path:
+                params["next_path"] = str(validate_next_path(next_path))
@@
-            # redirect to referer path
-            url = f"{base_host(request=request, is_space=True)}{str(next_path) if next_path else ''}"
+            # redirect to referer path
+            safe_next = str(validate_next_path(next_path)) if next_path else ""
+            # Clear transient session data
+            request.session.pop("state", None)
+            request.session.pop("next_path", None)
+            url = f"{base_host(request=request, is_space=True)}{safe_next}"

Also applies to: 49-53, 91-92

🤖 Prompt for AI Agents
In apps/api/plane/authentication/views/space/gitea.py around lines 27 (and also
apply to 49-53 and 91-92), the view stores and later echoes/redirects to
request.session["next_path"] without validation, enabling open-redirects or
unsafe output; validate and sanitize next_path before storing and before any use
by ensuring it is a safe relative path (no scheme, netloc, or leading '//' and
no path traversal like '..'), or if a full URL only allow known internal hosts,
otherwise replace with a safe default (e.g., "/") ; perform escaping when
injecting into error params to avoid unsafe output; update the code to parse the
candidate next_path, enforce these checks, store the sanitized value in session,
and re-validate/normalize it again right before adding to error params or
performing the final redirect, falling back to the safe default when validation
fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant