EddieHubCommunity / BioDrop

Connect to your audience with a single link. Showcase the content you create and your projects in one place. Make it easier for people to find, follow and subscribe.
https://biodrop.io
MIT License
5.72k stars 3.95k forks source link

[FEATURE] Rename profile #8435

Closed eddiejaoude closed 1 year ago

eddiejaoude commented 1 year ago

Description

When people change their GitHub username that does not match their LinkFree profile an alert should be shown with a button to rename their profile - with a warning that their url will change

Screenshots

No response

Additional information

No response

github-actions[bot] commented 1 year ago

To reduce notifications, issues are locked until they are https://github.com/EddieHubCommunity/LinkFree/labels/%F0%9F%8F%81%20status%3A%20ready%20for%20dev and to be assigned. You can learn more in our contributing guide https://github.com/EddieHubCommunity/LinkFree/blob/main/CONTRIBUTING.md

github-actions[bot] commented 1 year ago

The issue has been unlocked and is now ready for dev. If you would like to work on this issue, you can comment to have it assigned to you. You can learn more in our contributing guide https://github.com/EddieHubCommunity/LinkFree/blob/main/CONTRIBUTING.md

kunalshokeen051 commented 1 year ago

Hi, can i work on this issue.

SaraJaoude commented 1 year ago

@kunalshokeen051 great you are keen to contribute to the project, but this is a Points: 8 Issue so it is best if it done by someone who is more familiar with the project. We recommend new contributors start of with Issues which have Points: 1 to Points: 3

kunalshokeen051 commented 1 year ago

Ok, Thanks for the reply.

akash19coder commented 1 year ago

Please assign this issue to me. I guess I have spent a decent amount of time understanding the Linkfree codebase. I feel like I can work on this issue.

SaraJaoude commented 1 year ago

Please assign this issue to me. I guess I have spent a decent amount of time understanding the Linkfree codebase. I feel like I can work on this issue.

@akash19coder I have assigned this to you

akash19coder commented 1 year ago

Approach:

Step 1: Github Username Monitoring

GithubProvider({
    clientId: serverEnv.GITHUB_ID,
    clientSecret: serverEnv.GITHUB_SECRET,
    profile(profile) {
      return {
        id: profile.id.toString(), //we are talking about this id
        name: profile.name ?? profile.login,
        username: profile.login,
        email: profile.email,
        image: profile.avatar_url,
      };
    },
  })

Step 2: Alerting Mechanism

image

Step 3:

Step 4: URL Redirection (could be a separate issue)

eddiejaoude commented 1 year ago

Step 1: Github Username Monitoring Step 2: Alerting Mechanism

No monitoring is required, when they log in, we know their GitHub username, if this doesn't match their profile username we should show the alert. The alert should only be showed on the account statistics page

Step 3: When user clicks on the rename button, a warning message will pop up telling user that their BioDrop link will change. The user clicks on it and their new Github username will be fetched.

We have their new username, when they accept the warning message, we need to update all references to their old username to their new username

eddiejaoude commented 1 year ago

If we don't implement monitoring, we can only track the change in username if and only if the session has expired and the user re-logs in. Did I understand it correctly?

yes that is correct 👍 - I don't think people will change their username often, plus let's keep it simple for now, because at this moment in time we have nothing - we can add more functionality later if we need

akash19coder commented 1 year ago

[...nextAuth.js] file

import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";

import { serverEnv } from "@config/schemas/serverSchema";
import DbAdapter from "./db-adapter";
import connectMongo from "@config/mongo";
import { Account, Profile } from "@models/index";
import {
  getAccountByProviderAccountId,
  associateProfileWithAccount,
} from "../account/account";

let profileUsername = "";
let githubProfileUsername = "";
let profileID = "";

export const authOptions = {
  adapter: DbAdapter(connectMongo),
  providers: [
    GithubProvider({
      clientId: serverEnv.GITHUB_ID,
      clientSecret: serverEnv.GITHUB_SECRET,
      profile(profile) {
        return {
          id: profile.id.toString(),
          name: profile.name ?? profile.login,
          username: profile.login,
          email: profile.email,
          image: profile.avatar_url,
        };
      },
    }),
  ],
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async signIn({ user, profile: githubProfile }) {
      // console.log(user);
      // console.log(githubProfile);
      await Account.findOneAndUpdate(
        { userId: user._id },
        {
          github: {
            company: githubProfile.company,
            publicRepos: githubProfile.public_repos,
            followers: githubProfile.followers,
            following: githubProfile.following,
          },
        },
        { upsert: true }
      );
      return true;
    },
    async redirect({ baseUrl }) {
      return `${baseUrl}/account/statistics`;
    },
    async jwt({ token, account, profile }) {
      // Persist the OAuth access_token and or the user id to the token right after signin
      if (account) {
        account = await getAccountByProviderAccountId(profile.id);

        profile = await Profile.findOne({
          _id: account.profiles[0],
          // username: githubProfile.username,
        });
        token.accessToken = account.access_token;
        token.id = profile.id;
        token.username = profile.username;
      }

      return token;
    },
    async session({ session, token }) {
      // Send properties to the client, like an access_token and user id from a provider.
      session.accessToken = token.accessToken;
      session.user.id = token.id;
      session.username = token.username;

      return session;
    },
  },
  pages: {
    signIn: "/auth/signin",
  },
  events: {
    async signIn({ profile: githubProfile }) {
      // associate BioDrop profile to BioDrop account
      const account = await getAccountByProviderAccountId(githubProfile.id);
      const profile = await Profile.findOne({
        _id: account.profiles[0],
      });
      profileUsername = profile.username;
      githubProfileUsername = githubProfile.username;
      profileID = profile._id.toString();

      console.log(profileUsername);
      console.log(githubProfileUsername);
      console.log(profileID);

      if (profile) {
        await associateProfileWithAccount(account, profile._id);
      }
    },
  },
};

export { profileUsername, githubProfileUsername, profileID };

export default NextAuth(authOptions);
akash19coder commented 1 year ago

Statistics.js file

import { authOptions } from "../api/auth/[...nextauth]";
import { useState } from "react";
import { getServerSession } from "next-auth/next";
import dynamic from "next/dynamic";
import ProgressBar from "@components/statistics/ProgressBar";

import { getUserApi } from "../api/profiles/[username]";
import { clientEnv } from "@config/schemas/clientSchema";
import { getStats } from "../api/account/statistics";
import logger from "@config/logger";
import Alert from "@components/Alert";
import Page from "@components/Page";
import PageHead from "@components/PageHead";
import { abbreviateNumber } from "@services/utils/abbreviateNumbers";
import Navigation from "@components/account/manage/Navigation";
import UserMini from "@components/user/UserMini";
import ConfirmDialog from "@components/ConfirmDialog";
import { PROJECT_NAME } from "@constants/index";
import {
  profileUsername,
  githubProfileUsername,
  profileID,
} from "../api/auth/[...nextauth]";
import { Profile } from "@models/index";

console.log(Profile);
const DynamicChart = dynamic(
  () => import("../../components/statistics/StatsChart"),
  { ssr: false }
);

export async function getServerSideProps(context) {
  const { req, res } = context;
  const session = await getServerSession(req, res, authOptions);

  if (!session) {
    return {
      redirect: {
        destination: "/auth/signin",
        permanent: false,
      },
    };
  }
  const username = session.username;
  const { status, profile } = await getUserApi(req, res, username);
  if (status !== 200) {
    logger.error(
      profile.error,
      `profile loading failed for username: ${username}`
    );

    return {
      redirect: {
        destination: "/account/no-profile",
        permanent: false,
      },
    };
  }

  let data = {};
  let profileSections = [
    "links",
    "milestones",
    "tags",
    "socials",
    "testimonials",
  ];
  let progress = {
    percentage: 0,
    missing: [],
  };

  try {
    data = await getStats(username);
  } catch (e) {
    logger.error(e, "ERROR get user's account statistics");
  }

  progress.missing = profileSections.filter(
    (property) => !profile[property]?.length
  );
  progress.percentage = (
    ((profileSections.length - progress.missing.length) /
      profileSections.length) *
    100
  ).toFixed(0);

  data.links.individual = data.links.individual.filter((link) =>
    profile.links.some((pLink) => pLink.url === link.url)
  );

  const totalClicks = data.links.individual.reduce((acc, link) => {
    return acc + link.clicks;
  }, 0);
  data.links.clicks = totalClicks;

  data.profile.daily = data.profile.daily.map((day) => {
    return {
      views: day.views,
      date: day.date,
    };
  });

  return {
    props: {
      data,
      profile,
      progress,
      BASE_URL: clientEnv.NEXT_PUBLIC_BASE_URL,
    },
  };
}

export default function Statistics({ data, profile, progress, BASE_URL }) {
  const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);

  const openConfirmDialog = () => {
    setIsConfirmDialogOpen(true);
  };

  const handleRename = async () => {
    openConfirmDialog();
  };

  const renameUsername = async (
    profileUsername,
    githubProfileUsername,
    profileID
  ) => {
    if (profileUsername !== githubProfileUsername) {
      try {
        const updatedProfile = await Profile.findByIdAndUpdate(
          { _id: profileID },
          { username: githubProfileUsername },
          { new: true }
        );

        if (updatedProfile) {
          console.log(`Updated username to: ${updatedProfile.username}`);
        } else {
          console.log(`Profile with ID ${profileID} not found.`);
        }
      } catch (error) {
        console.error(`Error updating profile: ${error}`);
      }
    } else {
      console.log(`Profile username is already "${profileUsername}"`);
    }
  };

  return (
    <>
      <PageHead
        title={PROJECT_NAME + " Statistics"}
        description="Private statistics for your account"
      />

      <Page>
        <Navigation />
        <Alert
          type="info"
          message="We've detected change in your Github username. Please rename your biodrop profile with new changes"
          buttonLabel="Rename"
          onButtonAction={handleRename}
        />
        <ConfirmDialog
          open={isConfirmDialogOpen}
          setOpen={setIsConfirmDialogOpen}
          title="Your profile URL will change"
          description="Are you sure you want to rename?"
          buttonText="Rename"
          action={async () => {
            try {
              await renameUsername(
                profileUsername,
                githubProfileUsername,
                profileID
              );
              setIsConfirmDialogOpen(false); 
            } catch (error) {
              console.error("Error renaming username:", error);

            }
          }}
        />

        <UserMini
          BASE_URL={BASE_URL}
          username={profile.username}
          name={profile.name}
          bio={profile.bio}
          monthly={data.profile.monthly}
          total={data.profile.total}
          clicks={data.links.clicks}
          rank={data.profile.rank}
        />

        <div className="w-full border p-4 my-6 dark:border-primary-medium">
          <span className="flex flex-row flex-wrap justify-between">
            <span className="text-lg font-medium text-primary-medium dark:text-primary-low">
              Profile Completion: {progress.percentage}%
            </span>
            {progress.missing.length > 0 && (
              <span className="text-primary-medium-low">
                (missing sections in your profile are:{" "}
                {progress.missing.join(", ")})
              </span>
            )}
          </span>

          <ProgressBar progress={progress} />
        </div>

        {!data.links && (
          <Alert type="warning" message="You don't have a profile yet." />
        )}

        {data.profile.daily.length > 0 && (
          <div className="border mb-6 dark:border-primary-medium">
            <div className="border-b border-primary-low bg-white dark:bg-primary-high dark:border-primary-medium px-4 py-5 mb-2 sm:px-6">
              <h3 className="text-lg font-medium leading-6 text-primary-high">
                Profile views
              </h3>
              <p className="mt-1 text-sm text-primary-medium dark:text-primary-medium-low">
                Number of Profile visits per day.
              </p>
            </div>
            <DynamicChart data={data.profile.daily} />
          </div>
        )}

        <table className="min-w-full divide-y divide-primary-medium-low">
          <thead className="bg-primary-low dark:bg-primary-medium">
            <tr>
              <th
                scope="col"
                className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-primary-high dark:text-primary-low sm:pl-6"
              >
                Your Links ({data.links.individual.length})
              </th>
              <th
                scope="col"
                className="px-3 py-3.5 text-left text-sm font-semibold text-primary-high"
              >
                Clicks ({abbreviateNumber(data.links.clicks)})
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-primary-low dark:divide-primary-medium bg-white dark:bg-primary-high">
            {data.links &&
              data.links.individual.map((link) => (
                <tr key={link.url}>
                  <td className="md:whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-primary-high dark:text-primary-low sm:pl-6">
                    {link.url}
                  </td>
                  <td className="whitespace-nowrap px-3 py-4 text-sm text-primary-medium dark:text-primary-low">
                    {abbreviateNumber(link.clicks)}
                  </td>
                </tr>
              ))}
          </tbody>
        </table>
      </Page>
    </>
  );
}
akash19coder commented 1 year ago

@eddiejaoude I am getting an error like this. I tried to look it up but couldn't resolve it alone.

When I click on the rename button error occurs.

Screenshot (47)

Screenshot (46)

eddiejaoude commented 1 year ago

Please can we discuss in EddieHub Discord, it is easier than here.

Oh I just got a notification there also

akash19coder commented 1 year ago

@SaraJaoude I have unassigned myself because I have been a bit busy with my university tests and assignments for the past few weeks so I haven't been able to progress much on the issue and don't think I will be able to for the upcoming 10 days. Therefore, I have decided to unassign myself so that others can work if they want. I will ask for re-assignment if the issue is still unassigned after 10 days or definitely work on another if it's not 😅

btme0011 commented 1 year ago

Hey can i work on this issue. I have gone through the codebase we need to make changes here

async signIn({ profile: githubProfile }) { await connectMongo(); // associate BioDrop profile to account const account = await getAccountByProviderAccountId(githubProfile.id); const user = await User.findOne({ _id: account.userId });

  // associate User to Profile for premium flag
  const profile = await Profile.findOneAndUpdate(
    {
      username: githubProfile.username,
    },
    {
      user: account.userId,
    },
    {
      new: true,
    }
  );
  if (profile) {
    await associateProfileWithAccount(account, profile._id);
  }

  // Create a stripe customer for the user with their email address
  if (!user.stripeCustomerId) {
    logger.info("user stripe customer id not found for: ", user.email);

    const customer = await stripe.customers.create({
      email: user.email,
      name: user.name,
      metadata: {
        userId: account.userId,
        github: githubProfile.username,
      },
    });

    await User.findOneAndUpdate(
      { _id: new ObjectId(account.userId) },
      { stripeCustomerId: customer.id, type: "free" }
    );
  }
},

}, };

we will compare both the userId that we got while authentication and the userId in MongoDb and if they both are different we will show the warning message.

akash19coder commented 1 year ago

Hey can i work on this issue. I have gone through the codebase we need to make changes here

async signIn({ profile: githubProfile }) { await connectMongo(); // associate BioDrop profile to account const account = await getAccountByProviderAccountId(githubProfile.id); const user = await User.findOne({ _id: account.userId });

  // associate User to Profile for premium flag
  const profile = await Profile.findOneAndUpdate(
    {
      username: githubProfile.username,
    },
    {
      user: account.userId,
    },
    {
      new: true,
    }
  );
  if (profile) {
    await associateProfileWithAccount(account, profile._id);
  }

  // Create a stripe customer for the user with their email address
  if (!user.stripeCustomerId) {
    logger.info("user stripe customer id not found for: ", user.email);

    const customer = await stripe.customers.create({
      email: user.email,
      name: user.name,
      metadata: {
        userId: account.userId,
        github: githubProfile.username,
      },
    });

    await User.findOneAndUpdate(
      { _id: new ObjectId(account.userId) },
      { stripeCustomerId: customer.id, type: "free" }
    );
  }
},

}, };

we will compare both the userId that we got while authentication and the userId in MongoDb and if they both are different we will show the warning message.

Yep that's right @btme0011 . Currently, when there is mismatch user are unable to login. We wanna make little bit of changes so that user can login when there is username mismatch. May be something like this:

const profile = await Profile.findOne({
        _id: account.profiles[0],
      });

Then display a warning that username has changed with an option to update username. When user chooses to updata, may be execute a function that will updata database username with github username.

What do you think about it?

btme0011 commented 1 year ago

yes same thing is in my mind also.

Are you working on this isssue?

akash19coder commented 1 year ago

yes same thing is in my mind also.

Are you working on this isssue?

Not right now and don't think will be able to work in upcoming 5 days.

SaraJaoude commented 1 year ago

Thanks for the discussion. I am not sure whether you are looking to work on this issue @btme0011 but as you already have an issue assigned to you (we only assign one issue per contributor as shown in our Contributing Guide) I could not assign this to you. Also this has become higher priority so I am adding the staff label.

eddiejaoude commented 1 year ago

Closing because a duplicate of https://github.com/EddieHubCommunity/BioDrop/issues/7349