From 657dc4c74d30e7464e94e85fbf6fbf0629111d01 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 12 Dec 2025 14:55:38 +0530 Subject: [PATCH 1/9] feat: add sync functionality for OAuth providers - Implemented `check_sync_enabled` method to verify if sync is enabled for Google, GitHub, GitLab, and Gitea. - Added `sync_user_data` method to update user details, including first name, last name, display name, and avatar. - Updated configuration variables to include sync options for each provider. - Integrated sync check into the login/signup process. --- apps/api/plane/authentication/adapter/base.py | 66 ++++++++++++++++++- .../utils/instance_config_variables/core.py | 24 +++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/authentication/adapter/base.py b/apps/api/plane/authentication/adapter/base.py index d01f3f10b2b..868c80370da 100644 --- a/apps/api/plane/authentication/adapter/base.py +++ b/apps/api/plane/authentication/adapter/base.py @@ -90,9 +90,9 @@ def __check_signup(self, email): """Check if sign up is enabled or not and raise exception if not enabled""" # Get configuration value - (ENABLE_SIGNUP,) = get_configuration_value( - [{"key": "ENABLE_SIGNUP", "default": os.environ.get("ENABLE_SIGNUP", "1")}] - ) + (ENABLE_SIGNUP,) = get_configuration_value([ + {"key": "ENABLE_SIGNUP", "default": os.environ.get("ENABLE_SIGNUP", "1")} + ]) # Check if sign up is disabled and invite is present or not if ENABLE_SIGNUP == "0" and not WorkspaceMemberInvite.objects.filter(email=email).exists(): @@ -108,6 +108,30 @@ def __check_signup(self, email): def get_avatar_download_headers(self): return {} + def check_sync_enabled(self): + """Check if sync is enabled for the provider""" + if self.provider == "google": + (ENABLE_GOOGLE_SYNC,) = get_configuration_value([ + {"key": "ENABLE_GOOGLE_SYNC", "default": os.environ.get("ENABLE_GOOGLE_SYNC", "0")} + ]) + return ENABLE_GOOGLE_SYNC == "1" + elif self.provider == "github": + (ENABLE_GITHUB_SYNC,) = get_configuration_value([ + {"key": "ENABLE_GITHUB_SYNC", "default": os.environ.get("ENABLE_GITHUB_SYNC", "0")} + ]) + return ENABLE_GITHUB_SYNC == "1" + elif self.provider == "gitlab": + (ENABLE_GITLAB_SYNC,) = get_configuration_value([ + {"key": "ENABLE_GITLAB_SYNC", "default": os.environ.get("ENABLE_GITLAB_SYNC", "0")} + ]) + return ENABLE_GITLAB_SYNC == "1" + elif self.provider == "gitea": + (ENABLE_GITEA_SYNC,) = get_configuration_value([ + {"key": "ENABLE_GITEA_SYNC", "default": os.environ.get("ENABLE_GITEA_SYNC", "0")} + ]) + return ENABLE_GITEA_SYNC == "1" + return False + def download_and_upload_avatar(self, avatar_url, user): """ Downloads avatar from OAuth provider and uploads to our storage. @@ -208,6 +232,38 @@ def save_user_data(self, user): user.save() return user + def sync_user_data(self, user): + # Update user details + first_name = self.user_data.get("user", {}).get("first_name", "") + last_name = self.user_data.get("user", {}).get("last_name", "") + user.first_name = first_name if first_name else "" + user.last_name = last_name if last_name else "" + + # Get email + email = self.user_data.get("email") + + # Get display name + display_name = self.user_data.get("user", {}).get("display_name") + # If display name is not provided, generate a random display name + if not display_name: + display_name = User.get_display_name(email) + + # Set display name + user.display_name = display_name + + # Download and upload avatar + avatar = self.user_data.get("user", {}).get("avatar", "") + if avatar: + avatar_asset = self.download_and_upload_avatar(avatar_url=avatar, user=user) + if avatar_asset: + user.avatar_asset = avatar_asset + # If avatar upload fails, set the avatar to the original URL + else: + user.avatar = avatar + + user.save() + return user + def complete_login_or_signup(self): # Get email email = self.user_data.get("email") @@ -262,6 +318,10 @@ def complete_login_or_signup(self): # Create profile Profile.objects.create(user=user) + # Check if IDP sync is enabled + if self.check_sync_enabled(): + user = self.sync_user_data(user=user) + # Save user data user = self.save_user_data(user=user) diff --git a/apps/api/plane/utils/instance_config_variables/core.py b/apps/api/plane/utils/instance_config_variables/core.py index cf8d8d41fbe..4f4833207a5 100644 --- a/apps/api/plane/utils/instance_config_variables/core.py +++ b/apps/api/plane/utils/instance_config_variables/core.py @@ -44,6 +44,12 @@ "category": "GOOGLE", "is_encrypted": True, }, + { + "key": "ENABLE_GOOGLE_SYNC", + "value": os.environ.get("ENABLE_GOOGLE_SYNC", "0"), + "category": "GOOGLE", + "is_encrypted": False, + }, ] github_config_variables = [ @@ -65,6 +71,12 @@ "category": "GITHUB", "is_encrypted": False, }, + { + "key": "ENABLE_GITHUB_SYNC", + "value": os.environ.get("ENABLE_GITHUB_SYNC", "0"), + "category": "GITHUB", + "is_encrypted": False, + }, ] @@ -87,6 +99,12 @@ "category": "GITLAB", "is_encrypted": True, }, + { + "key": "ENABLE_GITLAB_SYNC", + "value": os.environ.get("ENABLE_GITLAB_SYNC", "0"), + "category": "GITLAB", + "is_encrypted": False, + }, ] gitea_config_variables = [ @@ -114,6 +132,12 @@ "category": "GITEA", "is_encrypted": True, }, + { + "key": "ENABLE_GITEA_SYNC", + "value": os.environ.get("ENABLE_GITEA_SYNC", "0"), + "category": "GITEA", + "is_encrypted": False, + }, ] smtp_config_variables = [ From c3f6d5b7d711a6b85cfbededa924d0005b04224d Mon Sep 17 00:00:00 2001 From: b-saikrishnakanth Date: Fri, 12 Dec 2025 18:01:36 +0530 Subject: [PATCH 2/9] feat: add sync toggle for OAuth providers in configuration forms --- .../(dashboard)/authentication/gitea/form.tsx | 46 +++++++++++++------ .../authentication/github/form.tsx | 46 +++++++++++++------ .../authentication/gitlab/form.tsx | 46 +++++++++++++------ .../authentication/google/form.tsx | 44 ++++++++++++------ .../components/common/controller-switch.tsx | 40 ++++++++++++++++ packages/types/src/instance/auth.ts | 19 ++++++-- 6 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 apps/admin/core/components/common/controller-switch.tsx diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx index 15b97b5880e..01d84328166 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx @@ -13,6 +13,8 @@ import { CodeBlock } from "@/components/common/code-block"; import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal"; import type { TControllerInputFormField } from "@/components/common/controller-input"; import { ControllerInput } from "@/components/common/controller-input"; +import type { TControllerSwitchFormField } from "@/components/common/controller-switch"; +import { ControllerSwitch } from "@/components/common/controller-switch"; import type { TCopyField } from "@/components/common/copy-field"; import { CopyField } from "@/components/common/copy-field"; // hooks @@ -41,6 +43,7 @@ export function InstanceGiteaConfigForm(props: Props) { GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com", GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"], GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"], + ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"] ?? "0", }, }); @@ -104,6 +107,11 @@ export function InstanceGiteaConfigForm(props: Props) { }, ]; + const GITEA_FORM_SWITCH_FIELD: TControllerSwitchFormField = { + name: "ENABLE_GITEA_SYNC", + label: "Gitea", + }; + const GITEA_SERVICE_FIELD: TCopyField[] = [ { key: "Callback_URI", @@ -130,20 +138,22 @@ export function InstanceGiteaConfigForm(props: Props) { const onSubmit = async (formData: GiteaConfigFormValues) => { const payload: Partial = { ...formData }; - await updateInstanceConfigurations(payload) - .then((response = []) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Done!", - message: "Your Gitea authentication is configured. You should test it now.", - }); - reset({ - GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value, - GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value, - GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value, - }); - }) - .catch((err) => console.error(err)); + try { + const response = (await updateInstanceConfigurations(payload)) || []; + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Done!", + message: "Your Gitea authentication is configured. You should test it now.", + }); + reset({ + GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value, + GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value, + GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value, + ENABLE_GITEA_SYNC: response.find((item) => item.key === "ENABLE_GITEA_SYNC")?.value, + }); + } catch (err) { + console.error(err); + } }; const handleGoBack = (e: React.MouseEvent) => { @@ -177,9 +187,15 @@ export function InstanceGiteaConfigForm(props: Props) { required={field.required} /> ))} +
- = { + name: "ENABLE_GITHUB_SYNC", + label: "GitHub", + }; + const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [ { key: "Origin_URL", @@ -152,20 +160,22 @@ export function InstanceGithubConfigForm(props: Props) { const onSubmit = async (formData: GithubConfigFormValues) => { const payload: Partial = { ...formData }; - await updateInstanceConfigurations(payload) - .then((response = []) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Done!", - message: "Your GitHub authentication is configured. You should test it now.", - }); - reset({ - GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value, - GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value, - GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value, - }); - }) - .catch((err) => console.error(err)); + try { + const response = (await updateInstanceConfigurations(payload)) || []; + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Done!", + message: "Your GitHub authentication is configured. You should test it now.", + }); + reset({ + GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value, + GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value, + GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value, + ENABLE_GITHUB_SYNC: response.find((item) => item.key === "ENABLE_GITHUB_SYNC")?.value, + }); + } catch (err) { + console.error(err); + } }; const handleGoBack = (e: React.MouseEvent) => { @@ -199,9 +209,15 @@ export function InstanceGithubConfigForm(props: Props) { required={field.required} /> ))} +
- = { + name: "ENABLE_GITLAB_SYNC", + label: "GitLab", + }; + const GITLAB_SERVICE_FIELD: TCopyField[] = [ { key: "Callback_URL", @@ -134,20 +142,22 @@ export function InstanceGitlabConfigForm(props: Props) { const onSubmit = async (formData: GitlabConfigFormValues) => { const payload: Partial = { ...formData }; - await updateInstanceConfigurations(payload) - .then((response = []) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Done!", - message: "Your GitLab authentication is configured. You should test it now.", - }); - reset({ - GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value, - GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value, - GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value, - }); - }) - .catch((err) => console.error(err)); + try { + const response = (await updateInstanceConfigurations(payload)) || []; + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Done!", + message: "Your GitLab authentication is configured. You should test it now.", + }); + reset({ + GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value, + GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value, + GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value, + ENABLE_GITLAB_SYNC: response.find((item) => item.key === "ENABLE_GITLAB_SYNC")?.value, + }); + } catch (err) { + console.error(err); + } }; const handleGoBack = (e: React.MouseEvent) => { @@ -181,9 +191,15 @@ export function InstanceGitlabConfigForm(props: Props) { required={field.required} /> ))} +
- = { + name: "ENABLE_GOOGLE_SYNC", + label: "Google", + }; + const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [ { key: "Origin_URL", @@ -140,19 +148,21 @@ export function InstanceGoogleConfigForm(props: Props) { const onSubmit = async (formData: GoogleConfigFormValues) => { const payload: Partial = { ...formData }; - await updateInstanceConfigurations(payload) - .then((response = []) => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Done!", - message: "Your Google authentication is configured. You should test it now.", - }); - reset({ - GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value, - GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value, - }); - }) - .catch((err) => console.error(err)); + try { + const response = (await updateInstanceConfigurations(payload)) || []; + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Done!", + message: "Your Google authentication is configured. You should test it now.", + }); + reset({ + GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value, + GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value, + ENABLE_GOOGLE_SYNC: response.find((item) => item.key === "ENABLE_GOOGLE_SYNC")?.value, + }); + } catch (err) { + console.error(err); + } }; const handleGoBack = (e: React.MouseEvent) => { @@ -186,9 +196,15 @@ export function InstanceGoogleConfigForm(props: Props) { required={field.required} /> ))} +
- = { + control: Control; + field: TControllerSwitchFormField; +}; + +export type TControllerSwitchFormField = { + name: FieldPath; + label: string; +}; + +export function ControllerSwitch(props: Props) { + const { + control, + field: { name, label }, + } = props; + + return ( +
+

Refresh user attributes from {label} during sign in

+
+ } + render={({ field: { value, onChange } }) => ( + (Boolean(parseInt(value)) === true ? onChange("0") : onChange("1"))} + size="sm" + /> + )} + /> +
+
+ ); +} diff --git a/packages/types/src/instance/auth.ts b/packages/types/src/instance/auth.ts index 5024849e900..6c29d8fdc9d 100644 --- a/packages/types/src/instance/auth.ts +++ b/packages/types/src/instance/auth.ts @@ -16,19 +16,28 @@ export type TInstanceAuthenticationMethodKeys = | "IS_GITLAB_ENABLED" | "IS_GITEA_ENABLED"; -export type TInstanceGoogleAuthenticationConfigurationKeys = "GOOGLE_CLIENT_ID" | "GOOGLE_CLIENT_SECRET"; +export type TInstanceGoogleAuthenticationConfigurationKeys = + | "GOOGLE_CLIENT_ID" + | "GOOGLE_CLIENT_SECRET" + | "ENABLE_GOOGLE_SYNC"; export type TInstanceGithubAuthenticationConfigurationKeys = | "GITHUB_CLIENT_ID" | "GITHUB_CLIENT_SECRET" - | "GITHUB_ORGANIZATION_ID"; + | "GITHUB_ORGANIZATION_ID" + | "ENABLE_GITHUB_SYNC"; export type TInstanceGitlabAuthenticationConfigurationKeys = | "GITLAB_HOST" | "GITLAB_CLIENT_ID" - | "GITLAB_CLIENT_SECRET"; - -export type TInstanceGiteaAuthenticationConfigurationKeys = "GITEA_HOST" | "GITEA_CLIENT_ID" | "GITEA_CLIENT_SECRET"; + | "GITLAB_CLIENT_SECRET" + | "ENABLE_GITLAB_SYNC"; + +export type TInstanceGiteaAuthenticationConfigurationKeys = + | "GITEA_HOST" + | "GITEA_CLIENT_ID" + | "GITEA_CLIENT_SECRET" + | "ENABLE_GITEA_SYNC"; export type TInstanceAuthenticationConfigurationKeys = | TInstanceGoogleAuthenticationConfigurationKeys From f1d87b301af94e2cf741352d4c552f8cff9813ec Mon Sep 17 00:00:00 2001 From: b-saikrishnakanth Date: Fri, 12 Dec 2025 18:15:02 +0530 Subject: [PATCH 3/9] fix: remove default value for sync options in OAuth configuration forms --- .../admin/app/(all)/(dashboard)/authentication/gitea/form.tsx | 4 ++-- .../app/(all)/(dashboard)/authentication/github/form.tsx | 4 ++-- .../app/(all)/(dashboard)/authentication/gitlab/form.tsx | 4 ++-- .../app/(all)/(dashboard)/authentication/google/form.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx index 01d84328166..910cab4ac1f 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx @@ -43,7 +43,7 @@ export function InstanceGiteaConfigForm(props: Props) { GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com", GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"], GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"], - ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"] ?? "0", + ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"], }, }); @@ -139,7 +139,7 @@ export function InstanceGiteaConfigForm(props: Props) { const payload: Partial = { ...formData }; try { - const response = (await updateInstanceConfigurations(payload)) || []; + const response = await updateInstanceConfigurations(payload); setToast({ type: TOAST_TYPE.SUCCESS, title: "Done!", diff --git a/apps/admin/app/(all)/(dashboard)/authentication/github/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/github/form.tsx index 0754a873bf9..2bfeb5ac2b3 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/github/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/github/form.tsx @@ -45,7 +45,7 @@ export function InstanceGithubConfigForm(props: Props) { GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"], GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"], GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"], - ENABLE_GITHUB_SYNC: config["ENABLE_GITHUB_SYNC"] ?? "0", + ENABLE_GITHUB_SYNC: config["ENABLE_GITHUB_SYNC"], }, }); @@ -161,7 +161,7 @@ export function InstanceGithubConfigForm(props: Props) { const payload: Partial = { ...formData }; try { - const response = (await updateInstanceConfigurations(payload)) || []; + const response = await updateInstanceConfigurations(payload); setToast({ type: TOAST_TYPE.SUCCESS, title: "Done!", diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitlab/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitlab/form.tsx index 91fe7a649c6..709ddd94b22 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitlab/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitlab/form.tsx @@ -43,7 +43,7 @@ export function InstanceGitlabConfigForm(props: Props) { GITLAB_HOST: config["GITLAB_HOST"], GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"], GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"], - ENABLE_GITLAB_SYNC: config["ENABLE_GITLAB_SYNC"] ?? "0", + ENABLE_GITLAB_SYNC: config["ENABLE_GITLAB_SYNC"], }, }); @@ -143,7 +143,7 @@ export function InstanceGitlabConfigForm(props: Props) { const payload: Partial = { ...formData }; try { - const response = (await updateInstanceConfigurations(payload)) || []; + const response = await updateInstanceConfigurations(payload); setToast({ type: TOAST_TYPE.SUCCESS, title: "Done!", diff --git a/apps/admin/app/(all)/(dashboard)/authentication/google/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/google/form.tsx index 802f2a454bd..971932a881d 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/google/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/google/form.tsx @@ -43,7 +43,7 @@ export function InstanceGoogleConfigForm(props: Props) { defaultValues: { GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"], GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"], - ENABLE_GOOGLE_SYNC: config["ENABLE_GOOGLE_SYNC"] ?? "0", + ENABLE_GOOGLE_SYNC: config["ENABLE_GOOGLE_SYNC"], }, }); @@ -149,7 +149,7 @@ export function InstanceGoogleConfigForm(props: Props) { const payload: Partial = { ...formData }; try { - const response = (await updateInstanceConfigurations(payload)) || []; + const response = await updateInstanceConfigurations(payload); setToast({ type: TOAST_TYPE.SUCCESS, title: "Done!", From 34db14b00552f7529ae2ece03e993e74e86072cd Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 15 Dec 2025 15:09:45 +0530 Subject: [PATCH 4/9] chore: delete old avatar and upload a new one --- apps/api/plane/authentication/adapter/base.py | 38 +++++++++++++------ apps/api/plane/db/models/user.py | 8 ++++ apps/api/plane/settings/storage.py | 12 ++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/apps/api/plane/authentication/adapter/base.py b/apps/api/plane/authentication/adapter/base.py index 868c80370da..358df5fcc05 100644 --- a/apps/api/plane/authentication/adapter/base.py +++ b/apps/api/plane/authentication/adapter/base.py @@ -21,6 +21,7 @@ from plane.utils.host import base_host from plane.utils.ip_address import get_client_ip from plane.utils.exception_logger import log_exception +from plane.settings.storage import S3Storage class Adapter: @@ -180,9 +181,6 @@ def download_and_upload_avatar(self, avatar_url, user): # Generate unique filename filename = f"{uuid.uuid4().hex}-user-avatar.{extension}" - # Upload to S3/MinIO storage - from plane.settings.storage import S3Storage - storage = S3Storage(request=self.request) # Create file-like object @@ -232,6 +230,22 @@ def save_user_data(self, user): user.save() return user + def delete_old_avatar(self, user): + """Delete the old avatar if it exists""" + try: + if user.avatar_asset: + asset = FileAsset.objects.get(pk=user.avatar_asset_id) + storage = S3Storage(request=self.request) + storage.delete_files(object_names=[asset.asset.name]) + user.avatar_asset = None + asset.delete() + user.save() + except FileAsset.DoesNotExist: + pass + except Exception as e: + log_exception(e) + return False + def sync_user_data(self, user): # Update user details first_name = self.user_data.get("user", {}).get("first_name", "") @@ -251,15 +265,16 @@ def sync_user_data(self, user): # Set display name user.display_name = display_name - # Download and upload avatar + # Download and upload avatar only if the avatar is different from the one in the storage avatar = self.user_data.get("user", {}).get("avatar", "") - if avatar: - avatar_asset = self.download_and_upload_avatar(avatar_url=avatar, user=user) - if avatar_asset: - user.avatar_asset = avatar_asset - # If avatar upload fails, set the avatar to the original URL - else: - user.avatar = avatar + # Delete the old avatar if it exists + self.delete_old_avatar(user=user) + avatar_asset = self.download_and_upload_avatar(avatar_url=avatar, user=user) + if avatar_asset: + user.avatar_asset = avatar_asset + # If avatar upload fails, set the avatar to the original URL + else: + user.avatar = avatar user.save() return user @@ -311,6 +326,7 @@ def complete_login_or_signup(self): avatar_asset = self.download_and_upload_avatar(avatar_url=avatar, user=user) if avatar_asset: user.avatar_asset = avatar_asset + user.avatar = avatar # If avatar upload fails, set the avatar to the original URL else: user.avatar = avatar diff --git a/apps/api/plane/db/models/user.py b/apps/api/plane/db/models/user.py index ee70032cf42..83f46b02ecb 100644 --- a/apps/api/plane/db/models/user.py +++ b/apps/api/plane/db/models/user.py @@ -167,6 +167,14 @@ def save(self, *args, **kwargs): super(User, self).save(*args, **kwargs) + @classmethod + def get_display_name(self, email): + return ( + email.split("@")[0] + if len(email.split("@")) == 2 + else "".join(random.choice(string.ascii_letters) for _ in range(6)) + ) + class Profile(TimeAuditModel): SUNDAY = 0 diff --git a/apps/api/plane/settings/storage.py b/apps/api/plane/settings/storage.py index 01afa62374f..4ebf6c58e80 100644 --- a/apps/api/plane/settings/storage.py +++ b/apps/api/plane/settings/storage.py @@ -187,3 +187,15 @@ def upload_file( except ClientError as e: log_exception(e) return False + + def delete_files(self, object_names): + """Delete an S3 object""" + try: + self.s3_client.delete_objects( + Bucket=self.aws_storage_bucket_name, + Delete={"Objects": [{"Key": object_name} for object_name in object_names]}, + ) + return True + except ClientError as e: + log_exception(e) + return False From a415f5bf2f8ac74ab8047aeaa8061b040d3d9a90 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 15 Dec 2025 15:20:23 +0530 Subject: [PATCH 5/9] chore: update class method --- apps/api/plane/db/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/plane/db/models/user.py b/apps/api/plane/db/models/user.py index 83f46b02ecb..9c82fba1e03 100644 --- a/apps/api/plane/db/models/user.py +++ b/apps/api/plane/db/models/user.py @@ -168,7 +168,7 @@ def save(self, *args, **kwargs): super(User, self).save(*args, **kwargs) @classmethod - def get_display_name(self, email): + def get_display_name(cls, email): return ( email.split("@")[0] if len(email.split("@")) == 2 From 1601f761871339a42181eb904d103f862b17a927 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 15 Dec 2025 15:21:33 +0530 Subject: [PATCH 6/9] chore: add email nullable --- apps/api/plane/db/models/user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/api/plane/db/models/user.py b/apps/api/plane/db/models/user.py index 9c82fba1e03..f9bd0e9bde6 100644 --- a/apps/api/plane/db/models/user.py +++ b/apps/api/plane/db/models/user.py @@ -169,6 +169,8 @@ def save(self, *args, **kwargs): @classmethod def get_display_name(cls, email): + if not email: + return "".join(random.choice(string.ascii_letters) for _ in range(6)) return ( email.split("@")[0] if len(email.split("@")) == 2 From 4576b201308cd384bb10dfc14c6d469670be0e8f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 17 Dec 2025 13:40:47 +0530 Subject: [PATCH 7/9] refactor: streamline sync check for multiple providers and improve avatar deletion logic --- apps/api/plane/authentication/adapter/base.py | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/apps/api/plane/authentication/adapter/base.py b/apps/api/plane/authentication/adapter/base.py index 358df5fcc05..baae95453b5 100644 --- a/apps/api/plane/authentication/adapter/base.py +++ b/apps/api/plane/authentication/adapter/base.py @@ -111,26 +111,16 @@ def get_avatar_download_headers(self): def check_sync_enabled(self): """Check if sync is enabled for the provider""" - if self.provider == "google": - (ENABLE_GOOGLE_SYNC,) = get_configuration_value([ - {"key": "ENABLE_GOOGLE_SYNC", "default": os.environ.get("ENABLE_GOOGLE_SYNC", "0")} - ]) - return ENABLE_GOOGLE_SYNC == "1" - elif self.provider == "github": - (ENABLE_GITHUB_SYNC,) = get_configuration_value([ - {"key": "ENABLE_GITHUB_SYNC", "default": os.environ.get("ENABLE_GITHUB_SYNC", "0")} - ]) - return ENABLE_GITHUB_SYNC == "1" - elif self.provider == "gitlab": - (ENABLE_GITLAB_SYNC,) = get_configuration_value([ - {"key": "ENABLE_GITLAB_SYNC", "default": os.environ.get("ENABLE_GITLAB_SYNC", "0")} - ]) - return ENABLE_GITLAB_SYNC == "1" - elif self.provider == "gitea": - (ENABLE_GITEA_SYNC,) = get_configuration_value([ - {"key": "ENABLE_GITEA_SYNC", "default": os.environ.get("ENABLE_GITEA_SYNC", "0")} - ]) - return ENABLE_GITEA_SYNC == "1" + provider_config_map = { + "google": "ENABLE_GOOGLE_SYNC", + "github": "ENABLE_GITHUB_SYNC", + "gitlab": "ENABLE_GITLAB_SYNC", + "gitea": "ENABLE_GITEA_SYNC", + } + config_key = provider_config_map.get(self.provider) + if config_key: + (enabled,) = get_configuration_value([{"key": config_key, "default": os.environ.get(config_key, "0")}]) + return enabled == "1" return False def download_and_upload_avatar(self, avatar_url, user): @@ -237,14 +227,18 @@ def delete_old_avatar(self, user): asset = FileAsset.objects.get(pk=user.avatar_asset_id) storage = S3Storage(request=self.request) storage.delete_files(object_names=[asset.asset.name]) - user.avatar_asset = None + + # Delete the user avatar asset.delete() + user.avatar_asset = None + user.avatar = "" user.save() + return except FileAsset.DoesNotExist: pass except Exception as e: log_exception(e) - return False + return def sync_user_data(self, user): # Update user details @@ -334,8 +328,8 @@ def complete_login_or_signup(self): # Create profile Profile.objects.create(user=user) - # Check if IDP sync is enabled - if self.check_sync_enabled(): + # Check if IDP sync is enabled and user is not signing up + if self.check_sync_enabled() and not is_signup: user = self.sync_user_data(user=user) # Save user data From 88562c5e1cd34631b0fcceff949ef55438708c7f Mon Sep 17 00:00:00 2001 From: b-saikrishnakanth Date: Wed, 17 Dec 2025 13:58:19 +0530 Subject: [PATCH 8/9] fix: ensure ENABLE_SYNC configurations default to "0" for Gitea, Github, Gitlab, and Google forms --- .../admin/app/(all)/(dashboard)/authentication/gitea/form.tsx | 3 ++- .../app/(all)/(dashboard)/authentication/github/form.tsx | 4 +--- .../app/(all)/(dashboard)/authentication/gitlab/form.tsx | 3 +-- .../app/(all)/(dashboard)/authentication/google/form.tsx | 3 +-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx index 68a07033aa4..7cad550f846 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/form.tsx @@ -42,7 +42,7 @@ export function InstanceGiteaConfigForm(props: Props) { GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com", GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"], GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"], - ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"], + ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"] || "0", }, }); @@ -191,6 +191,7 @@ export function InstanceGiteaConfigForm(props: Props) {